Mapa do sistema · três repos

Como o sistema funciona — e onde se mexe em cada coisa.

Um bot (inemaccbot) que não sabe nada sobre vídeo, e dois domínios (promoavatar, promoavatar3) que não sabem nada sobre filas. Esta página tem o diagrama de cada um, o mapa dos arquivos de configuração e a tabela de "quero mudar X, mexo onde".

1 · Visão geral

Cinco camadas, uma regra

A regra que sustenta o desenho está escrita no topo do src/fluxos/runtime.ts: quem orquestra não trabalha, quem trabalha não decide. O worker executa um job e o marca done/failed; quem lê isso e escolhe a próxima fase é o runtime — e faz isso dentro da mesma transação do ack, para que "fase feita" e "próxima fase enfileirada" nunca existam separadas.

CHAT GATEWAY FILA EXECUÇÃO ENTREGA Telegram gateway/telegram.ts · long polling Um chat pareado (ALLOWED_CHAT_IDS). Toda entrada do sistema passa por aqui. gateway/mensagem.ts — o roteador classifica a mensagem em quatro caminhos e devolve resposta em segundos (nada de trabalho pesado aqui) comandos.ts /fila /status /espaco /ping gramatica.ts skill: entrada | campos interpret.ts texto livre → agente curto comandos-fluxo.ts /promoavatar3 · /aprovar O interpret valida contra o registry: catálogo fechado. fila/store.ts — SQLite em WAL, uma fila durável jobs: queued → running → done | failed | canceled · lease com heartbeat · claim atômico · retomada depois de queda render · 1 navegador · 1 texto · 2 io · 10 cpu · 1 fila/filas.ts · CONCORRENCIAS worker.ts um por fila kind: agent runner-claude.ts · runner-chrome.ts kind: function fila/tarefas/* — sem modelo, sem prompt heygen.gerar · heygen.gerar-creditos · heygen.baixar · heygen.estudio reel.montar · ffmpeg.thumb · http.get (registradas em fila/tarefas/index.ts) fluxos/ runtime.ts lê o ack do job e enfileira a próxima fase na MESMA transação state/artefatos/ fonte canônica do bot publicar.ts renomeia p/ o título destinos.ts livesN → yt-pub-livesN/imports/videos notificar.ts avisa no chat
O caminho completo de uma mensagem. As duas caixas em âmbar são as fronteiras que mais importam: o gateway nunca trabalha, e o runtime é o único que decide qual é a próxima fase.

🚪 Gateway

Mede-se em segundos. Classifica a mensagem, valida contra o registry e enfileira. Se o agente do interpret alucinar um comando que não existe, a resposta é recusar — catálogo fechado.

🗄️ Fila

SQLite em WAL. Cinco classes de recurso com concorrência própria, para que um render não roube a vez de um download. Lease com heartbeat: se o processo cair, o job volta para a fila em vez de sumir.

⚙️ Execução

Duas naturezas de job. kind: agent chama um modelo com um prompt; kind: function é código TypeScript determinístico. Hoje só a fase de texto usa modelo — avatar e reel são função.

2 · Configuração

O bot não sabe nada sobre vídeo

Essa é a fronteira que faz o sistema crescer sem inchar. O bot conhece filas, portões, retentativa e entrega. O domínio conhece públicos, prompts, templates e o motor de render. Os dois se encontram em exatamente dois arquivos.

REPO DO BOT · ~/projetos/inemaccbot público · TypeScript · não conhece nenhum domínio REPO DE DOMÍNIO · ~/projetos/promoavatar3 irmão, não submódulo · JSON + prompts + Python config/fluxos.json registra o comando e diz QUAL repo carregar · lido no BOOT config/skills.json skills de uma etapa (transcrever, explicativo, reel…) · lido no BOOT src/dominio/flow.ts valida o flow.json · congela a definição · resolve variante e CTA src/fila/tarefas/index.ts catálogo de tarefas: o que `kind: function` pode chamar .env TELEGRAM_TOKEN · ALLOWED_CHAT_IDS · HEYGEN_API_KEY · PROJETOS_DIR state/ · inemaccbot.db fila, estado dos fluxos e artefatos — não se edita à mão flow.json alvos · fases · avatar/voz · templates · cta · motor_repo prompts/*.md o que a fase de texto escreve · variantes trocam este arquivo templates/*.json + templates/mapa.json layout do reel · e o mapa formato editorial → layout scripts/ (preparar.py · montar.py · montar-reel.py) o MOTOR de render — pode ser compartilhado via motor_repo cta/*.mp4 clipe de encerramento, escolhido por variante HELP.md · textos/ ajuda em duas camadas e a saída da fase de texto repo: valida
Só duas setas cruzam a fronteira. config/fluxos.json diz qual pasta carregar; o flow.ts valida o flow.json que veio de lá e recusa o que estiver malformado antes de qualquer job existir.
Regra de ouro para quem for montar outro sistema assim: o domínio referencia canal por NOME (lives2), nunca por caminho. Quem sabe onde isso fica no disco é o bot, num lugar só (src/dominio/destinos.ts). Um canal novo aparece só de criar a pasta yt-pub-livesN — sem editar nem recompilar o bot.

Quando o arquivo é lido

ArquivoLido quandoPrecisa reiniciar?
config/fluxos.jsonno boot do processoSim — e confira a fila vazia antes: restart mata render em voo
config/skills.jsonno boot do processoSim
flow.json do domíniona criação de cada fluxoNão
prompts/*.md do domíniona criação — o texto é congelado dentro do fluxoNão (mas fluxo já criado mantém o texto antigo)
templates/*.jsonna hora do render, pelo preparar.pyNão
.envno bootSim
3 · Anatomia

O que acontece entre o comando e o primeiro job

Um fluxo não é um script que roda de cima a baixo. Ele é uma definição que é congelada na criação e depois avança fase a fase, com o estado no banco. Isso é o que permite reiniciar o processo no meio e continuar de onde parou.

/promoavatar3 <assunto> | alvos=… | prompt=… | estudio criarFluxo valida o flow.json · recusa cedo comVariante() troca o prompt da fase congelar() embute o TEXTO dos prompts na definição A partir do congelamento o fluxo não depende mais do disco do domínio: editar o prompt hoje não muda um fluxo criado ontem, e o /refazer reusa a mesma definição. definicaoEfetiva — filtra as fases opcionais fases.filter(f => !f.opcional || opcoes[f.opcional]) uma fase marcada opcional só entra quando a flag foi pedida (| api, | estudio, | creditos) escopo: "fluxo" UM job para o fluxo inteiro. O prompt recebe {{publicos}} com TODOS os alvos e grava um arquivo por alvo em textos/. 1 chamada de modelo é o que mantém o custo baixo escopo: "alvo" UM job POR alvo — 12 no promoavatar, até 36 no promoavatar3. Cada um falha, retenta e é cancelado sozinho. N jobs paralelos limitados pela concorrência da fila pausa_apos: true — o portão humano Quando TODOS os jobs da fase terminam, o fluxo para e manda o resultado para o chat: os roteiros inteiros quando a fase é de escopo "fluxo", só a lista quando é de escopo "alvo". Só destrava com /aprovar C#23 · idempotente: aprovar duas vezes não duplica job
Os quatro conceitos que explicam qualquer fluxo: congelamento (imutabilidade), opcional (fases sob demanda), escopo (1 job ou N) e portão (revisão antes de gastar).

Os campos de uma fase no flow.json

CampoO que fazValores
idnome da fase — aparece no /statustexto, gerar, baixar, reel
escopo1 job por fluxo, ou 1 job por alvofluxo · alvo
filaclasse de recurso — define a concorrênciatexto io render navegador cpu
kindchama um modelo, ou roda códigoagent · function
tarefaqual tarefa do bot executafluxo-agente, heygen.gerar, reel.montar
promptarquivo do domínio (só em kind: agent)prompts/fase1-3versoes.md
variantesprompts alternativos, trocados por | prompt=<nome>{"viral": "prompts/fase1-viral.md"}
opcionala fase só entra se a flag foi pedidaapi estudio creditos navega
pausa_aposportão humano depois da fasetrue
esperapoll de trabalho externo{intervalo, timeout} em segundos
max_tentativasretentativa antes de marcar falha2, 3
4 · Domínio A

promoavatar — um vídeo por público

O domínio original. 12 alvos (um por público), prefixo A, sete fases das quais quatro são rotas opcionais alternativas para produzir o mesmo avatar.

prefixo A · alvos: pessoacomum, jovens, profissionais, mulheres, empreendedores, tecnicos, 40mais, 60mais, educadores, criadores, recolocacao, familia (12) texto agent · fila texto escopo fluxo · opus PORTÃO 12 roteiros vão inteiros pro chat quatro rotas para o MESMO avatar — escolha uma pela flag gerar function · io · | api gerar-creditos function · io · | creditos estudio function · navegador · | estudio navega-avatar agent · navegador · | navega só existe aqui — o promoavatar3 não tem baixar function · io escopo alvo PORTÃO poll até 40h reel function · render montar-reel.py publicar renomeia p/ o título copia p/ livesN
Sete fases, mas nunca sete jobs por alvo: as quatro rotas do meio são mutuamente exclusivas e só entram pela flag. Um fluxo típico roda texto → estudio → baixar → reel.
5 · Domínio C

promoavatar3 — três vídeos por público

O mesmo esqueleto, com o alvo virando <publico>-<tipo>: 12 públicos × 3 tipos = 36 alvos, prefixo C. Ganhou variantes de prompt, CTA por variante e o mapa de layout — e perdeu a rota navega-avatar.

alvo = <publico>-<tipo> · -alc alcance 25–40s (compartilhamento) · -aut autoridade 35–60s (salvamento) · -pro promocional 30–45s (CTA) cada alvo carrega no flow.json: canal (livesN) · gatilho (a dor daquele público) · fecho (o encerramento daquele tipo) texto agent · fila texto escopo fluxo 1 chamada → 36 arquivos PORTÃO variantes | prompt=manifesto | prompt=viral trocam o prompt da fase três rotas para o avatar gerar function · io · | api gerar-creditos function · io · | creditos estudio function · navegador · | estudio baixar function · io busca por título PORTÃO reel function · render montar-reel.py --flow --cta publicar nome = título copia p/ o canal do alvo cta: { padrao, viral } o clipe de encerramento é escolhido POR VARIANTE e por fluxo — nunca por tipo de alvo. Os 36 alvos recebem o mesmo clipe. src/dominio/flow.ts → ctaDaDefinicao()
A diferença estrutural para o promoavatar não é o número de fases — é o que o alvo significa. Aqui ele carrega tipo, canal, gatilho e fecho, e é isso que faz um assunto virar 36 roteiros diferentes numa chamada só.

promoavatar vs promoavatar3

promoavatarpromoavatar3
prefixoAC
alvo<publico> — 12<publico>-<tipo> — 36
fases7 (4 rotas opcionais)6 (3 rotas opcionais)
rota | navegatem (agente pilota o navegador)não tem
variantes de promptnãomanifesto, viral
CTAdefault do motorcta: {padrao, viral} por variante
modelo da fase textoopus fixado no flow.jsonperfil padrão do bot
timeout do reel10800s (3h)21600s (6h)
Cuidado ao editar os dois: os prompts divergiram sem ninguém decidir isso — cerca de 330 linhas idênticas foram editadas em triplicata. Se você mexer numa regra estrutural (as que o motor exige, como as seções SOBREPOSIÇÕES e IMAGENS), tem que replicar em todos os prompts dos dois repos. Nada no sistema força isso.
6 · Motor

O motor do reel — um motor, N domínios

A fase reel não renderiza nada: ela monta uma linha de comando e dispara um processo destacado. Todo o render mora em Python, no repo de domínio — e um domínio pode apontar para o motor de outro com motor_repo, em vez de copiar os scripts.

src/fila/tarefas/reel.ts montarComando() — só monta a string --avatar --ws --alvo --textos --saida --flow --cta bash -c destacado echo $$ > .pid → o /cancelar mata o processo certo || touch .err → falha em segundos scripts/montar-reel.py — no repo de DOMÍNIO o bot não sabe o que é um reel; só sabe disparar um script e vigiar o arquivo de saída motor_repo no flow.json permite um domínio usar o motor de outro, sem copiar OS SEIS PASSOS DO montar-reel.py 1/6 preparar mídia, transcrição, imagens, template, HTML 2/6 portão 1 lint + ritmo visual antes de gastar render 3/6 render HTML → frames → mp4 4/6 revisor áudio do render, silêncio, ritmo 5/6 CTA clipe de encerramento + entregável 6/6 QC portões 2 e 3 DENTRO DO PASSO 1 — scripts/preparar.py, uma chamada só 1. cria o workspace 2. lê duração/fps (ffprobe) 3. extrai o áudio e transcreve 4. detecta repetições 5. GERA AS IMAGENS da seção ## IMAGENS do roteiro 6. escreve manifesto.json 7. chama montar.py e entrega motion/index.html Existe porque o reel gastava 38 comandos de shell por vídeo — nenhum precisava de decisão. COMO O LAYOUT É ESCOLHIDO A fase de texto grava `Formato escolhido:` por alvo. templates/mapa.json traduz formato editorial → layout. --template > alvo.template > mapa[formato] > raiz Ninguém escolhe layout em tempo de render. TEMPLATES DISPONÍVEIS empilhado-capa (topo imagem + avatar + painel de texto) · diptico (metade/metade) · imagem-plena (avatar em recorte no topo-direita) · empilhado-explicativo (base = vídeo explicativo, mudo e em loop)
O motor é deliberadamente burro do lado do bot e esperto do lado do domínio. Isso é o que permite trocar o visual de todos os reels editando um JSON, sem recompilar nada.
7 · Receitas

Quero mudar X — mexo onde?

A tabela que responde 90% das perguntas de quem chega no sistema. A coluna da direita diz se é preciso reiniciar o processo.

Quero…Mexo emRestart?
Mudar o texto que a IA escreve<dominio>/prompts/fase1-*.mdnão
Acrescentar um público / alvo<dominio>/flow.jsonalvosnão
Trocar o canal de um público<dominio>/flow.jsonalvos.<alvo>.canalnão
Criar um canal novo (lives7)criar a pasta ~/projetos/yt-pub-lives7/imports/videosnão
Trocar o avatar ou a voz<dominio>/flow.jsonavatar_id, voice_id, enginenão
Mudar o visual do reel<dominio>/templates/*.jsonnão
Mudar qual layout cada formato usa<dominio>/templates/mapa.jsonnão
Cravar um layout para um públicoflow.jsonalvos.<alvo>.templatenão
Trocar o clipe de encerramento<dominio>/cta/*.mp4 + flow.jsonctanão
Criar uma variante de promptflow.jsonfases[texto].variantes + o novo .mdnão
Acrescentar uma fase ao pipelineflow.jsonfases (a tarefa já tem que existir no bot)não
Acrescentar uma tarefa novasrc/fila/tarefas/ + registrar em tarefas/index.tssim
Registrar um domínio novoconfig/fluxos.jsonsim
Registrar uma skill de uma etapaconfig/skills.json + prompts/<nome>.mdsim
Mudar a concorrência de uma filasrc/fila/filas.tsCONCORRENCIASsim
Trocar chave da HeyGen / token.envsim
Mudar a ajuda que aparece no chat<dominio>/HELP.mdnão

Um domínio novo, do zero, sem tocar no bot

Este é o teste de que a fronteira está no lugar certo. Se o seu domínio novo exigir editar TypeScript, ou ele precisa de uma tarefa que não existe, ou a fronteira vazou.

# 1. o repo do domínio, irmão do bot
mkdir ~/projetos/meudominio && cd ~/projetos/meudominio

# 2. flow.json — alvos, fases e o motor de outro 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 no bot — ÚNICO ponto que exige restart
#    config/fluxos.json:  {"command":"meudominio","repo":"meudominio",...}

# 4. testar SEM gastar avatar nem render
/meudominio assunto de teste | sombra
Antes de reiniciar o serviço: confira /fila e veja se há job running. Restart mata render em voo — e um reel de 6h de timeout recomeça do zero.
8 · Para quem for copiar o desenho

As decisões que fazem esse sistema funcionar

Não são detalhes de implementação — são as escolhas que evitaram classes inteiras de bug, e que valem para qualquer sistema de fila com agentes.

1 · Quem orquestra não trabalha

O worker executa e marca done/failed. Quem decide a próxima fase é o runtime, dentro da mesma transação do ack. Foi separar isso que acabou com o dispatch duplicado da versão anterior.

2 · Congelar a definição

O texto dos prompts é embutido no fluxo na criação. Editar o prompt hoje não altera um fluxo de ontem — e /refazer reproduz exatamente o que rodou.

3 · Catálogo fechado

O agente que interpreta texto livre pode alucinar um comando. A saída dele é validada contra o registry antes de virar job: comando que não existe é recusa, não tentativa.

4 · Instrução não é portão

Pedir no prompt não garante nada. Quando o agente do reel ignorou a seção IMAGENS e inventou os próprios prompts, a correção não foi escrever a regra em negrito — foi mover a geração das imagens para dentro do preparar.py. Só script, exit code ou caminho removido mudam comportamento.

5 · Portão antes de gastar

pausa_apos existe onde o próximo passo custa dinheiro ou horas. Revisar 36 roteiros no chat é barato; refazer 36 avatares não é.

6 · Nome, não caminho

O domínio diz lives2; só o bot sabe que isso é ~/projetos/yt-pub-lives2/imports/videos. Uma lista de canais em N cópias diverge; em um lugar só, não.

O que não existe — de propósito

Barreira entre fases, preempção de job, teto global de agentes e multiusuário. Cada ausência tem um gatilho documentado para ser reconsiderada. Um sistema que já nasce com tudo é um sistema que ninguém termina.