iArt.ai
AI 모션 그래픽 소프트웨어, 누구나 사용할 수 있는 After Effects 대안, 텍스트 프롬프트로 몇 분 안에 애니메이션·설명 영상 제작
아이디어를 영상으로 변환해 수직형으로 바로 게시 가능하게, 스크립트·내레이션·클립·자막을 AI가 몇 분 안에 만든다
Convierte una idea en un video vertical listo para publicar: guion, narración, clips y subtítulos generados con IA en minutos.
미디어TypeScript · JavaScriptGitHub @hanzmo16-png온라인운영자 미확인
써 보셨어요? 한 번만 눌러 주세요. 로그인은 필요 없어요.
아직 감지된 업데이트가 없습니다. GitHub 릴리스가 나오면 여기에 보입니다.
개인 계정의 공개 저장소 소유자는 GitHub로 로그인해 운영자를 확인하고 소개를 직접 관리할 수 있습니다. 관리자 승인은 필요하지 않습니다.
로그인한 뒤 확인 파일을 만들어 이 저장소의 기본 브랜치에 30분 안에 커밋해 주세요. 다른 브랜치나 포크의 파일은 인정하지 않습니다. 파일을 올린 뒤 아래에서 확인하면 관리자 승인 없이 관리할 수 있습니다. 자격을 유지하려면 파일을 남겨 두세요.
로그인이 필요하면 로그인 화면으로 이동합니다. 로그인 후 이곳으로 돌아와 원하는 확인 버튼을 다시 눌러 주세요.
프로젝트 폴더에서 /nomorevibe verify를 실행하면 소유권을 확인하고 소개를 직접 관리할 수 있습니다.
1분 만에 확인하기README 전체를 불러오는 중입니다. 아래는 저장된 발췌입니다.
Plataforma SaaS para generar reels "faceless" verticales (9:16), listos para TikTok/Instagram Reels/YouTube Shorts, a partir de un tema escrito por el usuario. El pipeline genera guion → voz → footage → música → subtítulos → ensamblado → render, con autenticación y suscripción de pago.README 전문 보기 ↗
Plataforma SaaS para generar reels "faceless" verticales (9:16), listos para TikTok/Instagram Reels/YouTube Shorts, a partir de un tema escrito por el usuario. El pipeline genera guion → voz → footage → música → subtítulos → ensamblado → render, con autenticación y suscripción de pago.
Frontend + backend: Next.js 16 (App Router, TypeScript), Tailwind CSS.
Auth + base de datos + storage: Supabase (Postgres, Auth, Storage).
Pagos: Stripe (suscripción mensual, restringe la generación).
Motor de composición: Remotion — renderiza el MP4 final (usa FFmpeg internamente para la codificación H.264/AAC). Se evaluó reescribir el ensamblado en FFmpeg puro, pero Remotion ya cumple "motor de composición modular", ya está integrado y verificado end-to-end en este repo — hacerlo de nuevo en FFmpeg crudo habría sido un retroceso sin beneficio real.
Pipeline modular por adaptadores (src/lib/providers/): cada etapa
(guion, voz, footage, música) tiene una interfaz común con dos
implementaciones intercambiables:
npm run test:pipeline) sin gastar
nada ni depender de conectividad externa.La selección de proveedor se auto-detecta por variable de entorno: si
falta la API key correspondiente, usa el fixture automáticamente (ver
.env.example).
Tema → Guion (adapter) → Voz (adapter) → Escenas alineadas a la narración
→ Footage por escena (adapter) → Música de fondo (adapter)
→ Subtítulos por frase → Render (Remotion) → Storage → Historial
git clone <url-del-repo>
cd atomivid
npm install
cp .env.example .env.local
Project URL, anon public key y
service_role key (esta última es secreta — nunca la expongas al cliente).http://localhost:3000/auth/callback (desarrollo)https://tu-dominio.com/auth/callback (producción, cuando aplique)En SQL Editor de Supabase, ejecuta en orden:
supabase/migrations/0001_init.sql — tabla video_requests con Row Level
Security (cada usuario solo ve/crea sus propias solicitudes).supabase/migrations/0002_generation.sql — columna error_message y el
bucket de Storage videos (lectura pública, escritura solo desde el
backend con la service role key).supabase/migrations/0003_billing.sql — tabla subscriptions (una fila
por usuario, RLS de solo lectura; las escrituras las hace el backend con
la service role key).supabase/migrations/0004_script_review.sql — columna script_json y el
estado script_ready, para el editor de guion antes del render final.supabase/migrations/0005_render_progress.sql — columna progress_stage,
para mostrar en qué parte del render va una generación en curso.supabase/migrations/0006_render_worker.sql — columnas para el worker en
background (render_attempts, render_started_at, render_worker,
video_path) y cambia el bucket videos de público a privado
(las URLs ahora se firman bajo demanda — ver "Worker en background").
Si ya habías generado algún video de prueba con la URL pública
anterior, esa URL deja de funcionar; no hay pérdida de datos, solo de
ese enlace.supabase/migrations/0007_input_limits.sql — topes de longitud/duración
en video_requests (defensa en profundidad para no permitir una
solicitud que dispare un gasto desproporcionado).supabase/migrations/0008_language_and_costs.sql — columna language
en video_requests (idioma elegido por el usuario) y tabla
generation_costs (registro de uso/costo estimado por video — ver
"Costo estimado por video" más abajo). Aplicada y confirmada en el
proyecto real de Supabase.supabase/migrations/0009_music_traceability.sql — columnas
music_track_id, music_track_title, music_track_author,
music_track_license, music_track_source_url y
music_fallback_reason en generation_costs, para saber qué pista
sonó en cada video (o por qué no sonó ninguna). Aplicada y
confirmada en el proyecto real de Supabase.Si usas la CLI de Supabase:
supabase link --project-ref <tu-project-ref>
supabase db push
MUSIC_TRACK_URL(S)
vacío para usar el fixture, o sigue esa sección para curar pistas reales
gratis.Si no configuras alguna de estas, esa etapa usa su proveedor fixture automáticamente — el pipeline completo sigue funcionando.
Crea una cuenta en dashboard.stripe.com (usa el modo Test mientras desarrollas).
En Product catalog, crea 3 productos (uno por paquete), cada
uno con un precio recurrente mensual — ver src/lib/billing/plans.ts
para el precio y la cuota exactos de cada uno:
Copia el Price ID (price_...) de cada uno a STRIPE_PRICE_ID_STARTER,
STRIPE_PRICE_ID_PRO y STRIPE_PRICE_ID_BUSINESS respectivamente.
Mientras alguna de las 3 no esté configurada, ese paquete se sigue
mostrando en /dashboard/billing pero falla con un error claro al
intentar suscribirse (no rompe la página ni los otros paquetes).
En Developers → API keys, copia la Secret key a STRIPE_SECRET_KEY.
Configura el webhook:
stripe listen --forward-to localhost:3000/api/stripe/webhook.
Te da un whsec_... — cópialo a STRIPE_WEBHOOK_SECRET.https://tu-dominio.com/api/stripe/webhook escuchando al menos
checkout.session.completed, customer.subscription.updated y
customer.subscription.deleted. Copia su Signing secret a
STRIPE_WEBHOOK_SECRET.npm run dev
Abre http://localhost:3000, crea una cuenta, confirma tu correo e inicia sesión.
4242 4242 4242 4242,
con stripe listen corriendo en paralelo si estás en local)./dashboard/review/[id]), donde puedes:
npm run test:pipeline
Corre el pipeline completo (guion "LOS RESULTADOS TIENEN UN PRECIO" con
~15 escenas, voz, footage, música y render) con los proveedores fixture, sin
red ni claves, y deja el resultado en scripts/atomivid-test-output.mp4
(no se sube al repo). Útil para verificar que la composición y el render
funcionan antes de gastar en APIs reales.
| Etapa | Real | Fixture (sin claves) | Variable de selección |
|---|---|---|---|
| Guion | Claude (Anthropic), salida estructurada | Texto templado en español | SCRIPT_PROVIDER |
| Voz | ElevenLabs, con timestamps por palabra | Tono generado (WAV) + timestamps sintéticos | VOICE_PROVIDER |
| Footage | Pexels (video vertical con respaldo en foto) | Imagen de color sólido con el texto de búsqueda | FOOTAGE_PROVIDER |
| Música | Biblioteca curada con selección por tono (MUSIC_MANIFEST) o playlist plana (MUSIC_TRACK_URL(S)) | Tono suave generado (WAV) | MUSIC_PROVIDER |
| Render | Remotion (Chromium + FFmpeg interno) | — (siempre real) | — |
Investigado antes de conectar nada (sin gastar ni contratar):
pixabay.com/api/docs) solo cubre imágenes y video; no tiene un
endpoint de audio/música. No hay forma de buscar/descargar música por
API con solo crear una cuenta — habría que descargar pistas manualmente
desde el sitio.[email protected] para acordar
licencia — no es autocontratable ni está garantizado que sea gratis.MUSIC_MANIFEST (procedencia, licencia, tono) y subirlas al bucket
privado music-library de Supabase Storage (nunca público — ver "Cómo
se resguardan las pistas" abajo). El proveedor curated-library
(src/lib/providers/music/real.ts, antes llamado custom-url) elige
entre ellas según el tono del video, no al azar puro — ver la sección
siguiente. Esto no requiere ninguna credencial nueva ni gasto; solo
curaduría manual de pistas (paso que le corresponde al usuario, ya que
implica elegir el estilo musical del producto) — ya en curso: el
bucket music-library está creado y MUSIC_MANIFEST tiene sus primeras
dos pistas reales, ver "Banco inicial" abajo para el detalle y lo que
falta.Con el manifest lleno, la música ya no se elige al azar sin contexto:
src/lib/providers/music/tone.ts): cada video se clasifica en
una o más de 10 categorías normalizadas (Motivacional, Corporativo,
Cinematográfico, Inspirador, Tensión, Reflexivo, Energético,
Minimalista, Tecnología, Lujo), combinando el estilo elegido en
/dashboard/new (señal base) con palabras clave detectadas en el tema
y en el guion ya generado (señal más fuerte — p. ej. "inteligencia
artificial" empuja hacia "Tecnología" aunque el estilo sea
"Curiosidades"). Determinístico, sin aleatoriedad: mismo input, mismo
tono siempre.select.ts): puntúa cada pista del manifest por
coincidencia de tono y elige entre las mejor puntuadas con una semilla
determinística (el requestId) — la misma solicitud siempre cae en la
misma pista (repetible para depurar), pero solicitudes distintas
normalmente caen en pistas distintas. Si ninguna pista coincide en
tono, cae a cualquiera del manifest en vez de fallar (variedad por
sobre precisión con un banco chico).validate.ts): antes de usar el archivo descargado, se
confirma por sus "magic bytes" que es un WAV/MP3/OGG real (no, por
ejemplo, una página de error HTML guardada con extensión .mp3 — causa
común de fallos silenciosos). No extrae duración/canales exactos de
MP3/OGG sin ffmpeg — limitación documentada, ver el archivo.remotion/audio-mix.ts, aplicada en VerticalReel.tsx vía
el prop volume de <Audio> de Remotion, que acepta una función por
frame — sample-accurate, sin pasos de audio externos): la voz queda
dominante (AUDIO_MIX
(audio-mix.ts) — nunca hardcodeados sueltos en el componente.errors.ts + generate.ts): el
proveedor distingue MusicNoMatchError (nada en el manifest),
MusicProviderError, MusicDownloadError y MusicInvalidFileError.
Cualquiera de estos hace que el video se genere sin música en vez
de fallar por completo — se registra en logs estructurados
([atomivid:music] ...) y en generation_costs.music_provider (queda
en ), nunca en silencio absoluto.Verificado con npm run test:unit (40 pruebas: tono, selección
determinística/variada, validación de archivos, fades/ducking sin saltos,
protección contra que la música supere a la voz, firma de URL de la
biblioteca de música) y con npm run test:pipeline (video real de
prueba, ver "Qué se verificó").
Pistas actualmente en MUSIC_MANIFEST (verificadas y cargadas el
2026-09-15, ambas con Pixabay Content License, en el bucket privado
music-library):
| Título | Autor | Tonos | Ruta en el bucket |
|---|---|---|---|
| Upbeat Corporate Inspiring | AudioCoffee | corporate, motivational, technology | pixabay-335162-upbeat-corporate-inspiring.mp3 |
| Instrumental music - powerful, motivational | Huynhhoa89 | motivational, energetic, cinematic | pixabay-266030-instrumental-music-powerful-motivational.mp3 |
Metadata completa (URL de origen, licencia exacta, fecha) vive en
src/lib/providers/music/manifest.ts. La primera pista trae además un
crédito sugerido por su página de origen ("Music by Denys Kyshchuk from
Pixabay."), distinto del perfil uploader (AudioCoffee) — documentado como
comentario en manifest.ts y en DECISIONS.md, porque MusicTrackEntry
todavía no tiene un campo estructurado para una línea de atribución
adicional a author, ni el pipeline genera créditos automáticamente en
ningún lado (subtítulos, descripción, etc.). Ampliar el esquema para esto
requiere autorización explícita antes de tocarlo.
Con el manifest ya no vacío, getMusicProvider() deja de caer al fixture
por defecto y scripts/render-worker.ts ya no rechaza un render real por
falta de música (antes sí lo hacía, mientras el manifest estaba vacío) —
pero esto todavía no se probó con una generación real contra Supabase:
ningún video se ha renderizado todavía usando estas dos pistas, ni se
confirmó que signMusicLibraryUrl() funcione contra el bucket real (ver
"Qué se verificó en producción real vs. solo localmente").
El tono genérico del proveedor fixture (un pad de dos tonos) sigue siendo solo para pruebas internas. Dos pistas ya cubren un banco mínimo, pero la investigación original recomendaba 10-15 para variedad completa entre estilos — para ampliarlo, yo no puedo hacerlo desde este entorno (sin salida de red hacia Pixabay/Mixkit), así que aquí están los pasos exactos:
Criterios para cada pista (los tres son obligatorios):
Pasos (el bucket music-library ya existe — creado, privado, sin
policies públicas — así que solo falta repetir esto por cada pista nueva):
/dashboard/new (Motivacional, Educativo, Humor, Historias de
terror, Curiosidades, Noticias / actualidad, Storytelling personal) —
no hace falta una pista distinta por estilo si una pista "neutra"
combina con varios.music-library en Supabase Storage. Nunca a este repositorio, y nunca
a un bucket público.src/lib/providers/music/manifest.ts (MUSIC_MANIFEST) con: id,
title, author (tal como lo indica la página de origen), sourceUrl
(la página exacta de donde la bajaste, para poder verificar la licencia
después), license (el nombre exacto, p. ej. "Pixabay Content License"), provider ("pixabay" o "mixkit", la fuente real —
distinto del proveedor de selección curated-library),
dateObtainedISO (fecha en que la descargaste/verificaste),
instrumental: true (obligatorio — nunca una pista con voz), tones
(uno o más valores de MusicTone, ver tone.ts — la señal que usa la
selección inteligente), styleTags (opcional, compatibilidad con el
selector de estilo literal) y storagePath (la ruta del archivo
dentro del bucket music-library, p. ej.
"pixabay-335162-upbeat-corporate-inspiring.mp3" — no una URL; el
pipeline firma una URL de lectura de máximo 1 hora bajo demanda, solo
en el servidor, ver "Cómo se resguardan las pistas" abajo).npm run test:pipeline que el pipeline sigue corriendo
(el manifest se usa automáticamente en cuanto tenga al menos una
entrada — no hace falta ninguna variable de entorno nueva).Las pistas de MUSIC_MANIFEST nunca quedan accesibles por una URL
permanente ni pública:
music-library es privado, sin ninguna policy de lectura
pública — solo la service role key puede leerlo (igual que el bucket
videos, ver DECISIONS.md).src/lib/providers/music/storage.ts firma una URL de lectura temporal
(máximo 1 hora, MUSIC_LIBRARY_SIGNED_URL_TTL_SECONDS) bajo demanda,
en el momento del render, solo desde código de servidor
(createServiceClient(), la misma service role key que ya usa el resto
del pipeline) — nunca se genera por adelantado ni se guarda.videos (mismo patrón que la voz y el
footage) — lo único que el usuario final ve alguna vez es la URL firmada
del video ya renderizado, nunca la de una pista individual.MusicObjectNotFoundError,
MusicSigningError, MusicDownloadError, MusicInvalidFileError) — el
video se genera igual sin música si cualquiera de estos ocurre (ver
"Selección inteligente de música y mezcla profesional" arriba).Presupuesto disponible: hasta $30,000 MXN, usado solo si una herramienta de pago resuelve algo que las opciones gratuitas no cubren — no se ha gastado nada de ese presupuesto todavía.
.env.example.Cada solicitud guarda su propio registro de uso/costo en la tabla
generation_costs (migración 0008), poblado automáticamente por el
pipeline — sin pasos manuales. Mide, por video:
voice_characters).script_calls, regenerations) y una estimación de tokens de
entrada/salida (script_estimated_input_tokens/_output_tokens).footage_count) y proveedor de cada etapa
(voice_provider, footage_provider, music_provider).video_duration_seconds) y tiempo de render
(render_ms).storage_bytes).estimated_cost_usd), incluyendo música
solo si music_provider no es "none" — la biblioteca curada actual es
gratis, así que suma $0 por defecto (PRICING_MUSIC_USD_PER_TRACK, ver
abajo); nunca se inventa un cargo por una pista gratuita.Las tarifas usadas para convertir eso a USD viven en
src/lib/billing/pricing.ts, con valores por defecto aproximados y
públicos — no son la tarifa real de tu cuenta. Configúralas con las
variables PRICING_* de .env.example (comentarios ahí mismo indican
dónde consultar tu tarifa vigente en cada proveedor). No es una
reconciliación exacta de factura — sirve para comparar el costo relativo
entre videos. Ver DECISIONS.md para el razonamiento completo.
Si el registro de costo falla por cualquier motivo (tabla no migrada, error de red), el video se genera igual — la instrumentación nunca bloquea el resultado, solo deja un warning en los logs.
Qué pista de música sonó en cada video, exactamente: además del nombre
del proveedor ("curated-library"/"fixture"/"none"), generation_costs
guarda por video music_track_id, music_track_title, music_track_author,
music_track_license, music_track_source_url (null si el video se generó
sin música o vino del modo "arranque rápido" sin manifest) y
music_fallback_reason (el motivo exacto cuando no hubo música — null si
sí la hubo) — columnas de supabase/migrations/0009_music_traceability.sql,
escritas por recordVideoGeneration (src/lib/billing/usage.ts). La misma
metadata también se emite en logs estructurados
([atomivid:music] pista seleccionada ..., src/lib/video/generate-video.ts).
Verificado: con un mock de Supabase (sin red), acumulación correcta de
llamadas/regeneraciones/caracteres y cálculo de estimated_cost_usd
(incluida la rama de música), y escritura correcta de las columnas
music_track_*/music_fallback_reason con datos simulados. Aplicado en
producción: las migraciones 0008 (language, generation_costs) y 0009
(music_track_*, music_fallback_reason) ya están aplicadas en el
proyecto real de Supabase — ambas confirmadas por el usuario. No
verificado todavía: una escritura real contra esa tabla en producción
con una pista de música real — MUSIC_MANIFEST ya tiene sus primeras dos
pistas, pero falta ejecutar una generación real de video para confirmarlo
(ver "Qué se verificó en producción real vs. solo localmente").
Implementado: /api/generate/[id]/render ya no corre el pipeline
dentro de la request HTTP. Ahora:
processing (con una
guarda de concurrencia optimista para evitar dos renders del mismo
video) y responde de inmediato (HTTP 202-equivalente)..github/workflows/render.yml vía la API de "repository
dispatch" de GitHub (src/lib/worker/github-actions.ts).scripts/render-worker.ts (que llama al mismo
runRenderJob()/pipeline que usa npm run test:pipeline), y actualiza
progress_stage/status/video_path en Supabase directamente.timeout-minutes: 12, un
paso if: failure() (scripts/mark-render-failed.ts) deja la
solicitud en failed en vez de processing para siempre.Si GH_WORKER_TOKEN/GH_WORKER_REPO no están configuradas, cae
automáticamente al worker inline (corre en el mismo proceso — el
comportamiento síncrono original, útil para desarrollo/pruebas locales;
ver npm run test:pipeline).
Verificado con datos reales (no solo fixtures): además de
npm run test:pipeline (fixtures, local), el workflow real se disparó
sobre una solicitud real en la base de datos de producción (creada vía
scripts/seed-test-request.ts, otro workflow de un solo uso, asociada a
un usuario real de Supabase Auth) y corrió de punta a punta con
credenciales reales: síntesis de voz con ElevenLabs, footage con Pexels,
render con Remotion/Chromium, subida a Supabase Storage. El video
resultante se verificó con ffprobe (corrido dentro del runner de GitHub
Actions, ya que este entorno de desarrollo tampoco tiene salida de red
hacia Supabase ni hacia el almacenamiento de artifacts de GitHub):
H.264, 1080×1920, 30fps, audio AAC, ~14MB.
Generación de guion real con Claude: verificada por separado
(scripts/test-real-script.ts, otro workflow de un solo uso) — guion e
regeneración de escena reales, con calidad y coherencia correctas
("10 minutos que reprograman tu cerebro", 6 escenas con narración y
búsqueda visual acordes al tema). src/lib/providers/script/real.ts
reintenta hasta 3 veces ante fallos transitorios de Claude (red, rate
limit); si sigue fallando, la solicitud queda en failed para
reintentar — deliberadamente no sustituye el contenido por el fixture en
producción (un guion templado no debe llegarle a un usuario real sin que
lo sepa, misma política que la música fixture).
Lo que sigue sin probarse: el disparo automático desde la app real
(un usuario hace clic en "Generar video final" en Vercel →
repository_dispatch vía GH_WORKER_TOKEN/GH_WORKER_REPO) — lo que se
probó fue el workflow en sí, disparado manualmente (workflow_dispatch)
apuntando a una solicitud real. Se investigó a fondo: el endpoint de
repository_dispatch (/repos/.../dispatches) también está bloqueado
por la política de red de este entorno de desarrollo, igual que las rutas
de Actions — no hay token ni mecanismo que lo evite desde aquí. Probarlo
de verdad requiere un clic real en la app desplegada.
Antes de implementar el workflow se verificó que repository_dispatch no
tiene la restricción de schedule en repos privados gratuitos (esa
restricción solo afecta a los triggers de cron); el tope real es 2,000
minutos/mes en repos privados y 6h por job — ambos muy por encima de lo
que necesita este MVP.
A continuación, la comparación completa de alternativas que llevó a elegir GitHub Actions para esta etapa:
| Opción | Costo mínimo/mes | Capa gratuita | Dificultad | Escalabilidad | Compatibilidad |
|---|---|---|---|---|---|
GitHub Actions (dispara un workflow desde la API, corre el render con el mismo Chromium/ffmpeg que ya usa test:pipeline, sube a Supabase Storage) | $0 | 2,000 min/mes gratis en repos privados (ilimitado en públicos) | Media — hay que orquestar: la API dispara el workflow vía GitHub API (token), el workflow corre el render y notifica de vuelta (webhook o polling del estado) | Baja/media — sin cola real, min limitados, latencia extra por arrancar el runner (~10-30s) | Alta — reutiliza el mismo código Node/Remotion/ffmpeg que ya corre en este repo, sin reescribir nada del pipeline |
| Remotion Lambda (AWS) | Pago por uso, sin mínimo fijo (AWS Lambda tiene capa gratis perpetua: 400,000 GB-s + 1M requests/mes) | Sí (capa gratis de AWS Lambda) | Media-alta — requiere cuenta AWS, configurar @remotion/lambda, S3 para assets/salida; Remotion en sí es gratis para equipos de ≤3 personas (nuestro caso), sin licencia de pago | Muy alta — es el producto oficial de Remotion para esto, pensado para producción | Alta — mismo motor de render que ya usamos, solo cambia dónde corre |
| Railway | ~$5-20+/mes reales para un worker persistente | Solo un trial de $5 una vez; después $1 de crédito gratis/mes (no alcanza para un worker que use Chromium) | Baja — despliegue simple tipo Heroku | Media | Alta — es solo un contenedor Node, correría el mismo código |
| Render.com | $7/mes (Starter, 512MB/0.5CPU) para un background worker | Free tier existe pero es para web services que se duermen a los 15 min de inactividad — no apto para un worker persistente que reciba jobs de render (que además necesita más de 512MB con Chromium) | Baja | Media | Alta — mismo modelo, contenedor Node |
Recomendación objetiva para validar el MVP con pocos usuarios y presupuesto ajustado: GitHub Actions como worker gratuito para la fase de validación (cero costo fijo, reutiliza exactamente lo ya construido y probado en este repo), y dejar Remotion Lambda como el camino a escalar cuando haya usuarios reales pagando — sigue siendo pago por uso (no una cuota fija mensual) y es la opción diseñada específicamente para esto por los mismos creadores de Remotion. Railway y Render.com no tienen ya una capa gratuita real para esta carga de trabajo (Chromium + render de video), así que solo tendrían sentido si se prefiere algo "siempre encendido" sin usar GitHub Actions — no se recomienda para esta etapa. Ninguna de estas dos opciones se ha implementado ni contratado — requiere tu decisión y, en el caso de Remotion Lambda, una cuenta de AWS con tarjeta de pago (aunque el uso esperado del MVP caiga dentro de la capa gratuita).
workflow_dispatch sobre una solicitud real en la
base de datos de producción. Video final verificado con ffprobe dentro
del propio runner de GitHub Actions (este entorno de desarrollo no tiene
salida de red hacia Supabase ni hacia el almacenamiento de artifacts de
GitHub, así que la verificación no se pudo hacer descargando el archivo
aquí — se corrió ffprobe en el runner y se leyó el resultado de los
logs): H.264, 1080×1920, 30fps, audio AAC, ~14MB. El disparo automático
real desde la app (repository_dispatch vía GH_WORKER_TOKEN/
GH_WORKER_REPO en Vercel) no se probó todavía — lo probado fue el
workflow en sí, disparado manualmente apuntando a una solicitud real.past_due/unpaid ya bloquea la
generación, pero no hay notificación proactiva al usuario
(invoice.payment_failed).timeout-minutes: 12 del job y el paso if: failure() lo
marcan como failed. Lo que no hay es un "reaper" activo con cron — un
render colgado que el runner no llega a matar (caso raro) queda en
processing hasta que el usuario intenta un nuevo render, momento en el
que la ruta API detecta que pasó RENDER_TIMEOUT_MS (15 min) y permite
reintentar. Aceptable para el volumen de un MVP; un cron sería la mejora
natural con más usuarios.api.anthropic.com y
registros de paquetes) — por eso las pruebas aquí usan los proveedores
fixture; el pipeline real con Supabase/ElevenLabs/Pexels/Stripe/GitHub
Actions solo se puede probar en producción (Vercel) o en una máquina con
salida de red normal.Completado y probado en producción real (no solo local/fixtures):
ffprobe corrido dentro del runner (H.264, 1080×1920, 30fps, audio AAC,
~14MB). Incluye idempotencia (guarda de concurrencia), límite de 3
reintentos, timeout de 15 min con aviso/reintento en el historial, y el
paso "marcar como fallido" si el workflow muere — confirmado en la
práctica (se activó correctamente en los primeros intentos fallidos
mientras se ajustaban credenciales).getSignedVideoUrl.scripts/test-real-script.ts — guion coherente y en tema, con
reintentos ante fallos transitorios (src/lib/providers/script/real.ts).@supabase/supabase-js necesita WebSocket
nativo), voz default "Rachel" ya no es gratuita por API en ElevenLabs
(cambiada a "Roger", confirmada como voz premade real de la cuenta), CRF
de Remotion sin acotar generaba videos más grandes que el límite de
Supabase Storage (bajado a crf: 26), y saldo insuficiente en la cuenta
de Anthropic (resuelto por el usuario agregando crédito).Completado y probado localmente (con fixtures):
<select>).npm run test:pipeline,
verificado con ffprobe: H.264 1080×1920 @30fps, audio AAC, crossfade,
subtítulos por frase, música mezclada, ~97s)./dashboard/review/[id]): editar texto y
búsqueda visual por escena (con topes de longitud/cantidad de escenas),
regenerar una escena individual, guardar cambios, y recién entonces
generar el video final.tone.ts, 10 categorías, estilo + palabras clave del tema/guion),
elección determinística/repetible con variedad entre videos
(select.ts), validación de archivo descargado (validate.ts), y
mezcla con fade-in/out + ducking suave en silencios de narración vía el
volume por frame de Remotion (remotion/audio-mix.ts) — ver
"Selección inteligente de música y mezcla profesional" arriba. Errores
tipados (errors.ts) con fallback a "video sin música" en vez de fallar
la generación completa. Las pistas del manifest se sirven desde el
bucket privado music-library (ya creado) con URL firmada bajo demanda
de máximo 1 hora, generada solo en el servidor (storage.ts) — nunca
una URL pública ni permanente, nunca expuesta al navegador.
MUSIC_MANIFEST tiene sus primeras dos pistas reales (ver "Música de
fondo: banco inicial" arriba) — sin probar todavía contra una
generación real (ver "Pendiente" abajo)./dashboard/new: se pasa
explícitamente al prompt de Claude (ya no se infiere del texto del
tema) y, opcionalmente, a una voz de ElevenLabs específica por idioma.
Verificado con fixtures (el guion generado confirma el idioma elegido).generation_costs, migración 0008): caracteres
de voz, llamadas al modelo de guion, regeneraciones, recursos visuales,
duración, tiempo de render, bytes de storage, música (0 si es gratis) y
costo total en USD según tarifas configurables
(src/lib/billing/pricing.ts). Verificado con un mock de Supabase
(acumulación y cálculo correctos, incluida la rama de música).npm run test:unit
(40 pruebas: tono, selección, validación de audio, mezcla, firma de URL
de la biblioteca de música) sin errores.Aplicado en producción:
supabase/migrations/0008_language_and_costs.sql — confirmado
(language_exists = true, generation_costs_exists = true).supabase/migrations/0009_music_traceability.sql — confirmado por el
usuario (existen las 6 columnas music_track_*/music_fallback_reason
en generation_costs).Pendiente (requiere al usuario o una decisión suya):
MUSIC_MANIFEST más allá de las 2 pistas actuales (bucket
music-library ya creado) hasta las ~10-15 recomendadas para variedad
completa entre estilos — instrucciones exactas en "Banco inicial"
arriba.music-library real, así que
signMusicLibraryUrl() y todo el flujo de descarga/mezcla siguen sin
verificarse fuera de las pruebas con mocks.MusicTrackEntry (p. ej. suggestedCredit?: string) si se necesita
mostrar créditos como "Music by Denys Kyshchuk from Pixabay." en algún
lugar visible — hoy solo está documentado en comentarios/DECISIONS.md,
sin campo ni generación automática.workflow_dispatch) sobre una solicitud sembrada directamente en la
base de datos, no el flujo completo iniciado por un clic real en la UI."[PRUEBA AUTOMÁTICA] Validación técnica del worker de Atomivid", usuario con email bajo atomivid-internal.test) cuando ya
no los necesites como referencia.Opcional pero recomendado antes de recibir clientes reales: sin esto, un
fallo en producción solo se detecta si alguien lo reporta a mano. La
integración (@sentry/nextjs, src/instrumentation.ts,
src/instrumentation-client.ts, src/sentry.server.config.ts,
src/sentry.edge.config.ts, withSentryConfig en next.config.ts) ya
está en el código y es un no-op seguro sin configurar — solo se activa si
defines las variables:
SENTRY_DSN y
NEXT_PUBLIC_SENTRY_DSN (mismo valor, dos variables porque una debe
llegar al navegador y la otra no).project:releases) y agrega
SENTRY_ORG, SENTRY_PROJECT, SENTRY_AUTH_TOKEN.Ver .env.example para el detalle de cada variable.
.env.example en Vercel (Project →
Settings → Environment Variables).NEXT_PUBLIC_SITE_URL a tu dominio de Vercel y agrega
<tu-dominio>/auth/callback como Redirect URL en Supabase.https://<tu-dominio>/api/stripe/webhook..github/workflows/render.yml:
SUPABASE_URL, SUPABASE_SERVICE_ROLE_KEY, ELEVENLABS_API_KEY,
ELEVENLABS_VOICE_ID (opcional), ELEVENLABS_MODEL_ID (opcional),
PEXELS_API_KEY, MUSIC_TRACK_URLS (opcional si ya llenaste
MUSIC_MANIFEST en código).GH_WORKER_TOKEN (un Personal Access Token de
GitHub con permiso para disparar workflows en este repo) y
GH_WORKER_REPO (owner/repo, p. ej. tu-usuario/Atomivid)./api/stripe/webhook) es la única fuente de verdad del estado
de la suscripción — la página de facturación solo lee lo que ya guardó el
webhook. Si en local no corres stripe listen, el checkout se completa en
Stripe pero la suscripción nunca se refleja en Atomivid.src/lib/billing/plans.ts) se cuenta por mes calendario, no por ciclo de
facturación de Stripe — más simple para el MVP.REMOTION_BROWSER_EXECUTABLE, Remotion descarga su propio Chromium la
primera vez (puede tardar). Si prefieres usar uno ya instalado, apunta esa
variable al ejecutable y, si es un chrome-headless-shell (recomendado en
Chrome >= 132, que quitó el "old headless mode"), agrega también
REMOTION_CHROME_MODE=headless-shell..env.example para ajustar los modelos usados.MUSIC_MANIFEST más allá de las 2 pistas actuales (bucket
music-library ya creado) y probar una generación real de video con
ellas contra el bucket real.repository_dispatch) y que el video aparece en el historial.ffprobe); Storage privado con URLs firmadas;
generación de guion y regeneración de escena con Claude real. Todo
disparado manualmente (workflow_dispatch) sobre una solicitud sembrada
directamente en la base de datos — no fue un clic real en la UI de
Vercel./dashboard/review/[id] → "Generar video final" →
repository_dispatch — investigado y confirmado que no se puede probar
desde este entorno bajo ningún token, ver "Worker en background"
arriba); cualquier transacción real de Stripe; y una generación real
usando las dos pistas reales ya cargadas en MUSIC_MANIFEST y el bucket
music-library (el render con ffprobe mencionado arriba se hizo antes
de que el manifest tuviera pistas, con música fixture). El código de
estas rutas es el mismo que ya se probó en las otras partes del
pipeline, pero esos caminos específicos no se ejercitaron todavía.api.anthropic.com, registros de paquetes, y la
API de GitHub a través del conector MCP de este chat) — por eso la
verificación del render real se hizo disparando workflows de GitHub
Actions y leyendo sus logs, en vez de probar la app directamente."none"