PTENES
Mapa del sistema · tres repos

Cómo funciona el sistema — y dónde se modifica en cada cosa.

Un bot (inemaccbot) que no sabe nada sobre video, y dos dominios (promoavatar, promoavatar3) que no saben nada sobre colas. Esta página muestra el diagrama de cada uno, el mapa de los archivos de configuración y la tabla de «quiero cambiar X, ¿dónde lo modifico?».

1 · Visión general

Cinco capas, una regla

La regla que sostiene el diseño está escrita en la parte superior del src/fluxos/runtime.ts: quien orquesta no trabaja; quien trabaja no decide. El worker ejecuta un job y lo marca done/failed; quien lee esto y elige la siguiente fase es el runtime — y lo hace dentro de la misma transacción del ack, para que «fase completada» y «siguiente fase encolada» nunca existan por separado.

CHAT GATEWAY COLA EJECUCIÓN ENTREGA Telegram gateway/telegram.ts · long polling Un chat vinculado (ALLOWED_CHAT_IDS). Toda entrada del sistema pasa por aquí. gateway/mensagem.ts — el enrutador clasifica el mensaje en cuatro rutas y devuelve una respuesta en segundos (aquí no se hace ningún trabajo pesado) comandos.ts /fila /status /espaco /ping gramatica.ts skill: entrada | campos interpret.ts texto libre → agente corto comandos-flujo.ts /promoavatar3 · /aprovar El interpret valida contra el registry: catálogo cerrado. fila/store.ts — SQLite en WAL, una cola duradera jobs: queued → running → done | failed | canceled · lease con heartbeat · claim atómico · reanudación después de una caída render · 1 navegador · 1 texto · 2 io · 10 cpu · 1 fila/filas.ts · CONCORRENCIAS worker.ts uno por fila kind: agent runner-claude.ts · runner-chrome.ts kind: function fila/tarefas/* — sin modelo, sin prompt heygen.gerar · heygen.gerar-creditos · heygen.baixar · heygen.estudio reel.montar · ffmpeg.thumb · http.get (registradas en fila/tarefas/index.ts) fluxos/ runtime.ts lee el ack del job y pone en cola el siguiente fase en la MISMA transacción state/artefatos/ fuente canónica del bot publicar.ts renombra según el título destinos.ts livesN → yt-pub-livesN/imports/videos notificar.ts avisa en el chat
El recorrido completo de un mensaje. Los dos recuadros en ámbar son los límites más importantes: el gateway nunca trabaja y el runtime es el único que decide cuál es la siguiente fase.

🚪 Gateway

Se mide en segundos. Clasifica el mensaje, lo valida contra el registry y lo pone en cola. Si el agente de interpret si alucina un comando que no existe, la respuesta es rechazarlo: catálogo cerrado.

🗄️ Cola

SQLite en WAL. Cinco clases de recursos con concurrencia propia, para que un render no le quite el turno a una descarga. Lease con heartbeat: si el proceso falla, el job vuelve a la cola en vez de desaparecer.

⚙️ Ejecución

Dos tipos de trabajo. kind: agent llama a un modelo con un prompt; kind: function es código TypeScript determinista. Hoy solo la fase de texto usa un modelo — avatar y reel son funciones.

2 · Configuración

El bot no sabe nada sobre video

Esta es la frontera que permite que el sistema crezca sin volverse pesado. El bot conoce las colas, las puertas de control, los reintentos y la entrega. El dominio conoce los públicos, los prompts, las plantillas y el motor de renderizado. Ambos se conectan en exactamente dos archivos.

REPO DEL BOT · ~/projetos/inemaccbot público · TypeScript · no conoce ningún dominio REPO DE DOMINIO · ~/projetos/promoavatar3 hermano, no submódulo · JSON + prompts + Python config/fluxos.json registra el comando y dice QUÉ repo cargar · leído en BOOT config/skills.json skills de una etapa (transcribir, explicativo, reel…) · leído en BOOT src/dominio/flow.ts valida el flow.json · congela la definición · resuelve variante y CTA src/fila/tarefas/index.ts catálogo de tareas: lo que `kind: function` puede llamar .env TELEGRAM_TOKEN · ALLOWED_CHAT_IDS · HEYGEN_API_KEY · PROJETOS_DIR state/ · inemaccbot.db cola, estado de los flujos y artefactos — no se edita a mano flow.json objetivos · fases · avatar/voz · templates · cta · motor_repo prompts/*.md lo que escribe la fase de texto · las variantes cambian este archivo templates/*.json + templates/mapa.json layout del reel · y el mapa formato editorial → layout scripts/ (preparar.py · montar.py · montar-reel.py) el MOTOR de render: puede compartirse mediante motor_repo cta/*.mp4 clip de cierre, elegido según la variante HELP.md · textos/ ayuda en dos capas y la salida de la fase de texto repo: valida
Solo dos flechas cruzan la frontera. config/fluxos.json indica qué carpeta cargar; el flow.ts valida el flow.json que vino de allí y rechaza lo que esté malformado antes de que exista cualquier job.
Regla de oro para quien vaya a montar otro sistema así: el dominio referencia el canal por NOMBRE (lives2), nunca por ruta. El bot es el único que sabe dónde está eso en el disco, en un solo lugar (src/dominio/destinos.ts). Un canal nuevo aparece con solo crear la carpeta yt-pub-livesN — sin editar ni recompilar el bot.

Cuando se lee el archivo

ArchivoSe lee cuando¿Hay que reiniciar?
config/fluxos.jsonal arrancar el procesoSí — y comprueba que la cola esté vacía antes: restart mata los renders en curso
config/skills.jsonal arrancar el procesoSí
flow.json del dominioal crear cada flujoNo
prompts/*.md del dominioal crear — el texto es congelado dentro del flujoNo (pero un flujo ya creado mantiene el texto anterior)
templates/*.jsonal momento del render, por medio de preparar.pyNo
.enval arrancarSí
3 · Anatomía

Qué ocurre entre el comando y el primer job

Un flujo no es un script que se ejecuta de principio a fin. Es una definición que congelada al crear y luego avanza fase a fase, con el estado en la base de datos. Esto es lo que permite reiniciar el proceso a mitad de camino y continuar desde donde se quedó.

/promoavatar3 <assunto> | alvos=… | prompt=… | estudio crearFlujo valida el flow.json · rechaza pronto comVariante() cambia el prompt de la fase congelar() incrusta el TEXTO de los prompts en la definición A partir de la congelación, el flujo ya no depende del disco del dominio: editar el prompt hoy no cambia un flujo creado ayer, y el /refazer reutiliza la misma definición. definicaoEfetiva — filtra las fases opcionales fases.filter(f => !f.opcional || opcoes[f.opcional]) una fase marcada como opcional solo entra cuando se solicita el flag (| api, | estudio, | creditos) escopo: "fluxo" UN job para todo el flujo. El prompt recibe {{publicos}} con TODOS los objetivos y guarda un archivo por objetivo en textos/. 1 llamada al modelo es lo que mantiene bajo el costo escopo: "alvo" UN job POR objetivo — 12 en promoavatar, hasta 36 en promoavatar3. Cada uno falla, vuelve a intentarlo y se cancela por separado. N jobs paralelos limitados por la concurrencia de la cola pausa_apos: true — la puerta humana Cuando terminan TODOS los jobs de la fase, el flujo se detiene y envía el resultado al chat: los guiones completos cuando la fase es de alcance "flujo", solo la lista cuando es de alcance "objetivo". Solo se desbloquea con /aprovar C#23 · idempotente: aprobar dos veces no duplica el job
Los cuatro conceptos que explican cualquier flujo: congelación (inmutabilidad), opcional (fases bajo demanda), alcance (1 job o N) y puerta (revisión antes de gastar).

Los campos de una fase en el flow.json

CampoQué haceValores
idnombre de la fase: aparece en /statustexto, gerar, baixar, reel…
escopo1 job por flujo, o 1 job por objetivofluxo · alvo
filaclase de recurso — define la concurrenciatexto io render navegador cpu
kindllama a un modelo o ejecuta códigoagent · function
tarefaqué tarea ejecuta el botfluxo-agente, heygen.gerar, reel.montar…
promptarchivo del dominio (solo en kind: agent)prompts/fase1-3versoes.md
variantesprompts alternativos, intercambiados por | prompt=<nome>{"viral": "prompts/fase1-viral.md"}
opcionalla fase solo se ejecuta si se solicitó la flagapi estudio creditos navega
pausa_apospuerta humana después de la fasetrue
esperapoll de trabajo externo{intervalo, timeout} en segundos
max_tentativasreintento antes de marcar fallo2, 3
4 · Dominio A

promoavatar — un video por público

El dominio original. 12 objetivos (uno por público), prefijo A, siete fases, de las cuales cuatro son rutas opcionales alternativas para producir el mismo avatar.

prefijo A · objetivos: pessoacomum, jovens, profissionais, mulheres, empreendedores, tecnicos, 40mais, 60mais, educadores, criadores, recolocacao, familia (12) texto agente · cola de texto alcance del flujo · opus PUERTA Se envían 12 guiones enteros para el chat cuatro rutas para el MISMO avatar: elige una con la flag generar function · io · | api generar-créditos función · io · | créditos estudio función · navegador · | estudio navega-avatar agente · navegador · | navega solo existe aquí — promoavatar3 no tiene descargar function · io alcance del objetivo PUERTA poll hasta 40h reel función · render montar-reel.py publicar renombra según el título copia para livesN
Siete fases, pero nunca siete jobs por objetivo: las cuatro rutas intermedias son mutuamente excluyentes y solo se activan con la flag. Un flujo típico ejecuta texto → estudio → baixar → reel.
5 · Dominio C

promoavatar3 — tres videos por público

El mismo esqueleto, con el objetivo convertido en <publico>-<tipo>: 12 públicos × 3 tipos = 36 objetivos, prefijo C. Ganó variantes de prompt, CTA por variante y el mapa de layout — y perdió la ruta navega-avatar.

objetivo = <publico>-<tipo> · -alc alcance 25–40s (compartir) · -aut autoridad 35–60s (guardar) · -pro promocional 30–45s (CTA) cada objetivo lleva en flow.json: canal (livesN) · disparador (el dolor de ese público) · cierre (el final de ese tipo) texto agente · cola de texto alcance del flujo 1 llamada → 36 archivos PUERTA variantes | prompt=manifesto | prompt=viral cambian el prompt de la fase tres rutas para el avatar generar function · io · | api generar-créditos función · io · | créditos estudio función · navegador · | estudio descargar function · io búsqueda por título PUERTA reel función · render montar-reel.py --flow --cta publicar nombre = título copia para el canal del objetivo cta: { padrao, viral } el clip de cierre se elige POR VARIANTE y por flujo — nunca por tipo de objetivo. Los 36 objetivos reciben el mismo clip. src/dominio/flow.ts → ctaDaDefinicao()
La diferencia estructural con promoavatar no es el número de fases — es lo que el objetivo significa. Aquí carga tipo, canal, activador y cierre, y eso es lo que hace que un tema se convierta en 36 guiones diferentes en una sola llamada.

promoavatar vs promoavatar3

promoavatarpromoavatar3
prefijoAC
objetivo<publico> — 12<publico>-<tipo> — 36
fases7 (4 rutas opcionales)6 (3 rutas opcionales)
ruta | navegatiene (el agente pilota el navegador)no tiene
variantes de promptnomanifesto, viral
CTAvalor predeterminado del motorcta: {padrao, viral} por variante
modelo de la fase de textoopus fijado en el flow.jsonperfil predeterminado del bot
timeout del reel10800s (3h)21600s (6h)
Ten cuidado al editar los dos: los prompts divergieron sin que nadie lo decidiera: se editaron cerca de 330 líneas idénticas por triplicado. Si modificas una regla estructural (de las que exige el motor, como las secciones SOBREPOSIÇÕES e IMAGENS), hay que replicarlo en todos los prompts de los dos repos. Nada en el sistema lo impone.
6 · Motor

El motor del reel: un motor, N dominios

La fase reel no renderiza nada: arma una línea de comandos y ejecuta un proceso en segundo plano. Todo el render se hace en Python, en el repo de dominio; y un dominio puede apuntar al motor de otro con motor_repo, en vez de copiar los scripts.

src/fila/tarefas/reel.ts montarComando() — solo arma la cadena --avatar --ws --alvo --textos --saida --flow --cta bash -c resaltado echo $$ > .pid → o /cancelar mata el proceso correcto || touch .err → falla en segundos scripts/montar-reel.py — en el repo de DOMINIO el bot no sabe qué es un reel; solo sabe ejecutar un script y vigilar el archivo de salida motor_repo en flow.json permite que un dominio use el motor de otro, sin copiar LOS SEIS PASOS DE montar-reel.py 1/6 preparar medios, transcripción, imágenes, template, HTML 2/6 puerta 1 lint + ritmo visual antes de gastar en el render 3/6 render HTML → frames → mp4 4/6 revisor audio del render, silencio, ritmo 5/6 CTA clip de cierre + entregable 6/6 QC puertas 2 y 3 DENTRO DEL PASO 1 — scripts/preparar.py, una sola llamada 1. crea el workspace 2. lee la duración/fps (ffprobe) 3. extrae el audio y lo transcribe 4. detecta repeticiones 5. GENERA LAS IMÁGENES de la sección ## IMAGENS del guion 6. escribe manifesto.json 7. llama a montar.py y entrega motion/index.html Existe porque el reel consumía 38 comandos de shell por video; ninguno requería tomar decisiones. CÓMO SE ELIGE EL DISEÑO La fase de texto guarda `Formato escolhido:` por objetivo. templates/mapa.json traduce el formato editorial → layout. --template > alvo.template > mapa[formato] > raiz Nadie elige el layout durante el renderizado. PLANTILLAS DISPONIBLES empilhado-capa (imagen arriba + avatar + panel de texto) · diptico (mitad y mitad) · imagem-plena (avatar en recorte arriba a la derecha) · empilhado-explicativo (base = video explicativo, sin sonido y en bucle)
El motor es deliberadamente simple del lado del bot e inteligente del lado del dominio. Esto permite cambiar el aspecto de todos los reels editando un JSON, sin recompilar nada.
7 · Recetas

Quiero cambiar X: ¿dónde lo modifico?

La tabla que responde el 90% de las preguntas de quienes llegan al sistema. La columna de la derecha indica si hace falta reiniciar el proceso.

Quiero…Cambio¿Restart?
Cambiar el texto que escribe la IA<dominio>/prompts/fase1-*.mdno
Agregar un público / objetivo<dominio>/flow.json → alvosno
Cambiar el canal de un público<dominio>/flow.json → alvos.<alvo>.canalno
Crear un canal nuevo (lives7)crear la carpeta ~/projetos/yt-pub-lives7/imports/videosno
Cambiar el avatar o la voz<dominio>/flow.json → avatar_id, voice_id, engineno
Cambiar el aspecto del reel<dominio>/templates/*.jsonno
Cambiar el layout que usa cada formato<dominio>/templates/mapa.jsonno
Definir un diseño para un públicoflow.json → alvos.<alvo>.templateno
Cambiar el clip de cierre<dominio>/cta/*.mp4 + flow.json → ctano
Crear una variante de promptflow.json → fases[texto].variantes + el nuevo .mdno
Agregar una fase al pipelineflow.json → fases (la tarea ya debe existir en el bot)no
Agregar una tarea nuevasrc/fila/tarefas/ + registrar en tarefas/index.tssí
Registrar un dominio nuevoconfig/fluxos.jsonsí
Registrar una skill de una etapaconfig/skills.json + prompts/<nome>.mdsí
Cambiar la concurrencia de una colasrc/fila/filas.ts → CONCORRENCIASsí
Cambiar la clave de HeyGen / token.envsí
Cambiar la ayuda que aparece en el chat<dominio>/HELP.mdno

Un dominio nuevo, desde cero, sin tocar el bot

Esta es la prueba de que la frontera está en el lugar correcto. Si tu nuevo dominio exige editar TypeScript, necesita una tarea que no existe o la frontera se filtró.

# 1. el repo del dominio, junto al bot
mkdir ~/projetos/meudominio && cd ~/projetos/meudominio

# 2. flow.json — objetivos, fases y el motor de otro repo
cat > flow.json <<'JSON'
{
  "nome": "meudominio", "prefixo": "M", "versao_def": 1,
  "motor_repo": "promoavatar3",
  "templates_dir": "templates", "template": "empilhado-capa",
  "alvos": { "ep01": { "canal": "lives2", "fecho": "..." } },
  "fases": [
    { "id": "texto", "escopo": "fluxo", "fila": "texto",
      "kind": "agent", "tarefa": "fluxo-agente",
      "prompt": "prompts/fase1.md", "pausa_apos": true },
    { "id": "reel", "escopo": "alvo", "fila": "render",
      "kind": "function", "tarefa": "reel.montar" }
  ]
}
JSON

# 3. registrar en el bot — ÚNICO punto que requiere reiniciar
# config/fluxos.json:  {"command":"meudominio","repo":"meudominio",...}

# 4. probar SIN gastar avatar ni render
/meudominio assunto de teste | sombra
Antes de reiniciar el servicio: revisa /fila y comprueba si hay un job running. Restart termina el render en curso — y un reel con 6h de timeout vuelve a empezar desde cero.
8 · Para quien vaya a copiar el diseño

Las decisiones que hacen que este sistema funcione

No son detalles de implementación: son las decisiones que evitaron clases enteras de errores y que sirven para cualquier sistema de colas con agentes.

1 · Quien orquesta no trabaja

El worker ejecuta y marca done/failed. Quien decide la siguiente fase es el runtime, dentro de la misma transacción del ack. Separar eso fue lo que acabó con el dispatch duplicado de la versión anterior.

2 · Congelar la definición

El texto de los prompts se incorpora al flujo al crearlo. Editar el prompt hoy no altera un flujo de ayer, y /refazer reproduce exactamente lo que se ejecutó.

3 · Catálogo cerrado

El agente que interpreta texto libre puede alucinar un comando. Su salida se valida contra el registry antes de convertirse en job: un comando que no existe es un rechazo, no un intento.

4 · La instrucción no es una puerta

Pedirlo en el prompt no garantiza nada. Cuando el agente del reel ignoró la sección IMAGENS e inventó sus propios prompts, la corrección no fue escribir la regla en negrita: fue mover la generación de imágenes dentro del preparar.py. Solo el script, el exit code o la ruta eliminada cambian el comportamiento.

5 · Puerta antes de gastar

pausa_apos existe donde el siguiente paso cuesta dinero u horas. Revisar 36 guiones en el chat es barato; rehacer 36 avatares no lo es.

6 · Nombre, no ruta

El dominio dice lives2; solo el bot sabe que esto es ~/projetos/yt-pub-lives2/imports/videos. Una lista de canales en N copias diverge; en un solo lugar, no.

Qué no existe, a propósito

Barrera entre fases, preempción de jobs, límite global de agentes y multiusuario. Cada ausencia tiene un detonante documentado para reconsiderarla. Un sistema que nace con todo es un sistema que nadie termina.