Lição 23 · Fusão v3 · @alembic/marketing-factory ← índice
Alembic × Hermes — O Curso de Fusão v3 · Percepção & Fábrica (21–25)

@alembic/marketing-factory: a fábrica de anúncios multi-tenant

Uma fábrica de vídeo brand-agnóstica: a PASTA do cliente dirige tudo — identidade, ativos reais, restrições — e o código não muda por cliente (provada em 3 indústrias: advocacia, odontologia, contabilidade). Modelos FIXOS (gemini_omni para vídeo pt-BR com lipsync, gpt_image_2 para imagem), o unblock das referências ROLED via API direta (provado com o escritório REAL da C.D), e a regra que paga as contas: uma flag sozinha NUNCA gasta — só --approve E --yes juntas.

1

A tese: a pasta do cliente dirige, o código não muda

produto · 0006-marketing-factory · s3

O pacote nasceu internalizando o "Supercomputer" do Higgsfield atrás de uma camada anticorrupção — e a decisão estrutural foi ser multi-tenant desde o tipo: sinal, brief, marca, script, restrições e ativos são INPUTS por cliente. O clientBriefSchema (packages/marketing-factory/src/types.ts:871) carrega o grupo identity — location, characters, wardrobe, scenarios, sceneObjects, nonVerbal, referenceAssets (types.ts:925–928, :850) — mais a marca com spokenName (types.ts:895) e os proibidos forbiddenVisuals/forbiddenClaims (types.ts:898).

A entrada é AUTO-detectada na CLI: um JSON com objeto client que passa no clientBriefSchema.safeParse é um ClientBrief; sem client, é um BusinessSignal fino — "a brief is recognised only when clientBriefSchema parses" (apps/cli/src/commands.ts:2903–2926). Um brief é projetado de volta em signal via briefToSignal, então a fábrica existente consome os dois sem bifurcar código.

Pense como… uma gráfica de moldes: o molde (pipeline) é um só; o que muda de pedido para pedido é a arte que o cliente traz na pasta. Onde a analogia quebra: aqui a "arte" inclui rosto, uniforme, parede do escritório — e o molde tem travas que RECUSAM imprimir sem aprovação dupla.

discover ask → brief client pasta → identidade environment·sheets ativos TRAVADOS antes video · ad gemini_omni validate QA lição 21/22 campaign one-shot TODOS os estágios nascem DRY-RUN / $0 — a mesma corrente roda inteira sem gastar um centavo o capstone `marketing ad <pasta> <brief>` funde a corrente num comando; `campaign` faz discover→generate→validate
A corrente da fábrica — cada elo é um comando `alembic marketing …`; a pasta do cliente é o único input que muda entre tenants.
As pontas da corrente: discover extrai o spec do vídeo de um pedido em texto livre com parseAsk determinístico e um port de pesquisa injetável cujo default é o no-op honesto offlineBriefResearch (discover.ts:132, :65) — a proveniência só vira online-research quando algo foi DE FATO aprendido (discover.ts:244–257). client lê o brand-profile.json CURADO da pasta (folder-ingest.ts:196) e só descobre ativos no subdir vouched de --assets-dir — uma pasta real de cliente NUNCA é varrida às cegas para dentro de PII de processos; isExcludedDir pula clientes/contratos/etc. como segunda cerca (folder-ingest.ts:78, :217–222). environment/sheets constroem as folhas de referência do espaço real e dos personagens (environment-sheet.ts:116, asset-sheets.ts:478) — com a regra de fidelidade: rosto REAL nunca é copiado para um personagem (buildFaceErasePrompt, asset-sheets.ts:217).
2

Identidade injetada: o bloco + as referências reais (≤7)

Duas peças fazem um render sair "inconfundivelmente do cliente" em vez de genérico. A primeira é TEXTO: buildIdentityContext(brief) (identity.ts:19–80) monta UM bloco determinístico — marca (com a forma falada), o que faz, público, localização, voz, tom, personagens, figurino, cenários, objetos de cena, cores — e fecha com os LOCKS invioláveis: "nunca mostrar: …" / "nunca afirmar/implicar: …" (identity.ts:60–67). O bloco é PREPENDADO a todo prompt criativo. Um brief fino (só nome) injeta NADA — byte-idêntico ao comportamento pré-identidade (identity.ts:20–23, 69–70).

A segunda é IMAGEM: identityRefAssetIds(brief) (identity.ts:98–106) junta os assets reais já upados (logo, escritório, uniforme, produto) e o refAssetId de cada personagem — pulando avatares sintéticos — e o chamador aplica o teto do modelo: MAX_REFERENCE_IMAGES = 7 (ads.ts:103), resolvido por modelo via referenceCapForModel (ads.ts:118–120), porque o limite de 7 é do gemini_omni (ads.ts:208).

O unblock que destravou tudo (2026-07-04)O CLI real do Higgsfield passa refs como --image "flat" — e o gemini_omni REJEITA esse formato no create (medias[].GeminiOmniReferenceImageMedia.role: Field required — documentado em higgsfield-api.ts:18–21). A saída foi reproduzir o caminho do PRÓPRIO site: no caminho pago, os assets viajam pela API direta como medias ROLED — { role: 'image', data: { id, type: 'media_input' } } (higgsfield-api.ts:134–135, :167). PROVADO na fronteira: a parede do escritório real da C.D Advocacia + o logo apareceram no render 720×1280. O mesmo shape roled serve o gpt_image_2 (higgsfield-api.ts:119–120).
packages/marketing-factory/src/identity.ts:60–67 (os LOCKS, trecho real)
const locks: string[] = [];
if (brand.forbiddenVisuals.length > 0) {
  locks.push(`nunca mostrar: ${brand.forbiddenVisuals.join(', ')}`);
}
if (brand.forbiddenClaims.length > 0) {
  locks.push(`nunca afirmar/implicar: ${brand.forbiddenClaims.map((c) => `"${c}"`).join(', ')}`);
}
if (locks.length > 0) lines.push(`- LOCKS (invioláveis): ${locks.join('; ')}`);
3

Modelos fixos + aspect como PARÂMETRO, não texto

Decisão de founder, não default acidental: vídeo é SEMPRE gemini_omni (Gemini Omni Flash) — DEFAULT_VIDEO_MODEL_ID = 'gemini_omni' (ads.ts:95), resolvido por modelId = opts.modelId ?? DEFAULT_VIDEO_MODEL_ID (ads.ts:172) — pela fala pt-BR com lipsync; imagem é gpt_image_2 (o builder de job dedicado em higgsfield-api.ts:119–135). Um operador pode pinar outro modelo via --model-id; sem pino, a fábrica não "escolhe" — obedece.

E a lição cara de aspect ratio: o modelo IGNORA "9:16" escrito no prompt e rende landscape. A correção é estrutural — --aspect viaja como o parâmetro REAL aspect_ratio de geração (buildVideoSceneParams, ads.ts:136–149), passado VERBATIM para o modelo SELECIONADO validar: o enum do gemini_omni é só 16:9,9:16 (verificado via higgsfield model get gemini_omni, citado em ads.ts:87); um valor fora do enum falha fechado ANTES de gastar.

SuperfícieModelo fixoOnde está cravado
Vídeo (video · ad · campaign)gemini_omni — pt-BR + lipsyncads.ts:95, :172
Imagem (sheets · environment recreate)gpt_image_2higgsfield-api.ts:119–135
Refs por geração≤ 7 (teto do gemini_omni)ads.ts:103, :118–120, :208
4

A dupla trava: uma flag sozinha NUNCA gasta

produto · 0006-marketing-factory · s1

O contrato de gasto está escrito no topo de ads.ts: "Spend-safe by construction: dryRun defaults to true … Real generation requires BOTH dryRun: false and approve: true" (ads.ts:28–31). No código: const dryRun = opts.dryRun ?? true (ads.ts:173) e o portão if (dryRun || !opts.approve) devolve o preview de custo (estimateCost SEMPRE roda primeiro, ads.ts:163) sem tocar o createGeneration pago (ads.ts:245). Na CLI isso vira as duas flags --approve E --yes — e o mesmo par governa marketing ad, sheets e campaign.

cena do shotlist estimateCost SEMPRE o portão de DUAS chaves dryRun:false approve:true ads.ts:173 + :245 — E lógico, nunca OU AMBAS createGeneration pago (refs ROLED) falha ⇒ err fail-closed; nunca gasto cego 1 só / 0 preview de custo — $0, nada rendido dryRun: dryRun || !approve (ads.ts:316) o mesmo par --approve + --yes governa video · ad · sheets · campaign — uma flag sozinha é SEMPRE preview
ads.ts — spend-safe por construção: estimate primeiro, gasto só com as duas chaves viradas juntas.
Você roda alembic marketing video shotlist.json --approve (sem --yes). O que a fábrica faz?
Correto: o portão é if (dryRun || !opts.approve) com dryRun default true (ads.ts:173, :245) — falta a segunda chave, então o caminho pago nem é alcançado; sai o preview com estimateCost. Não é erro: o dry-run É o produto do caminho sem aprovação dupla.
5

ViralityScorer, manifest versionado e o MCP offline

runMarketingFactory (flow.ts:78+) fecha o ciclo de um signal: copy/GTM → criativos → filtro do ViralityScorerAssetsManifest versionado e content-addressed (flow.ts:282–283). O scorer é um PORT (flow.ts:45) com default honesto offlineViralityScorer (flow.ts:58, escolhido em :345) — kept/rejected sem fingir um preditor que não existe; o hash do manifest permanece determinístico sobre inputs idênticos (flow.ts:263).

E a fábrica também fala MCP — offline por default: MARKETING_MCP_TOOLS expõe as 6 capacidades do Supercomputer (marketing_virality_prediction, marketing_video_analyzer, marketing_video_generator, marketing_image_to_video, marketing_soul_training, marketing_viral_clip_generatormcp.ts:49–62). As 3 ferramentas de crédito repetem a dupla trava por chamada (dryRun:false E approve:true, mcp.ts:56, :258–261) — e mesmo aprovada, uma chamada com os deps OFFLINE default FALHA FECHADA pedindo createOnlineMarketingMcpDeps({ higgsfield }) (mcp.ts:177, :566): gastar exige injetar o backend real DE PROPÓSITO, duas vezes.

Por fim, o conhecimento operacional vive como skill no próprio pacote: packages/marketing-factory/skills/ carrega ads-factory.md (o orquestrador de 4 estágios: assets → shotlist → cenas → QA) e seedance-shotlist-director/SKILL.md (a skill canônica do founder para autorar shotlists do Estágio 2, internalizada verbatim) — as DUAS skills canônicas do pacote (prova: ls packages/marketing-factory/skills/; o diretório também guarda os playbooks de copy que o loader loadMarketingSkill injeta como contexto — skills.ts:28+).

Via MCP, alguém chama marketing_video_generator com dryRun:false, approve:true — mas com os deps DEFAULT do surface. Resultado?
Correto: as flags autorizam, mas o BACKEND pago precisa ser injetado explicitamente (createOnlineMarketingMcpDeps, mcp.ts:177); com os deps offline default a chamada aprovada retorna erro claro em vez de fingir (mcp.ts:566). Duas camadas independentes: intenção (flags) e capacidade (deps).
CAMADA TÉCNICA — comandos copy-paste

Toda a corrente roda $0 por default — estes comandos NUNCA gastam:

# descobrir um brief offline a partir de um pedido em texto livre
alembic marketing discover request.json --out brief.json

# ingerir a pasta do cliente (perfil curado; assets só do subdir vouched)
alembic marketing client ~/clientes/cd-advocacia --assets-dir brand-assets --out brief.json

# preview $0 de cenas (dupla trava fechada = só estimativa de custo)
alembic marketing video shotlist.json --aspect 9:16 --client ~/clientes/cd-advocacia

# o capstone e o one-shot — ambos dry-run por default
alembic marketing ad ~/clientes/cd-advocacia brief.json --aspect 9:16
alembic marketing campaign request.json --out plan.json

# provas no código: modelo fixo, teto de refs, dupla trava, medias roled
grep -n 'DEFAULT_VIDEO_MODEL_ID\|MAX_REFERENCE_IMAGES' packages/marketing-factory/src/ads.ts
sed -n '243,258p' packages/marketing-factory/src/ads.ts
sed -n '14,25p;130,140p' packages/marketing-factory/src/higgsfield-api.ts
grep -n 'LOCKS' packages/marketing-factory/src/identity.ts

# as skills canônicas do pacote
ls packages/marketing-factory/skills/
O que levar desta lição
Pergunta de acompanhamento sugerida: "o que acontece se o brief pedir um visual que os LOCKS proíbem?" — siga o fio: o lock entra no prompt, e o validate da lição 21 varre os frames com forbiddenVisuals — cinto E suspensório. Próxima lição: de onde vem a CARA de tudo isso — o design-system Warm-Neutral como dado tipado e o gate impeccable que o governa no CI.