Curso · produto

Alembic — Marketing Factory

Marketing Factory — discovery → generate → validate

O @alembic/marketing-factory não é "uma fábrica de ad para C.D Advocacia". É um pipeline brand-agnostic em três estágios — discover (aprender um ClientBrief rico a partir de um pedido fino), generate (rodar o motor sobre o BusinessSignal projetado, atrás de uma anti-corruption layer sobre o CLI real do higgsfield) e validate (plano de QA, ou a QA real sobre um mp4 baixado) — e é spend-safe por construção: tudo roda dryRun: true / $0 por padrão, com três travas opt-in independentes onde uma flag sozinha nunca gasta.

O pipeline de três estágios e as três travas

discover request → ClientBrief parseAsk + research? generate briefToSignal → factory HiggsfieldCli seam validate ValidationPlan (dry) validateVideo (real) trava 1 — discovery default: offline ($0) opt-in: --online → brightdata real trava 2 — generation default: fake CLI ($0) opt-in: --approve AND --yes → higgsfield real (paid) trava 3 — validation default: plan-only opt-in: validateVideo → sobre mp4 baixado invariante: uma flag sozinha NUNCA gasta

Os três estágios são composáveis e cada um carrega sua própria trava. Você pode rodar só discover (e ter um ClientBrief JSON para revisar antes de gerar), só generate (a partir de um signal já curado), só validate (sobre um mp4 que veio de outro lugar), ou os três num único shot via campaign. O capstone é marketing campaign, que orquestra os três com todas as travas ativas em simultâneo: discovery offline + factory com fake cli + validation plan-only = $0 / zero rede / zero spend.

marketing discover — aprender um ClientBrief

Um ClientBrief é a unidade de input multi-tenant da factory. Um request é fino — só { client: { name, website? }, ask } — e o discoverClientBrief (em packages/marketing-factory/src/discover.ts) o expande para o brief rico que o resto da factory consome:

{
  "client": { "name": "C.D Advocacia", "website": "https://cd.adv.br" },
  "ask": "vídeo vertical cinematográfico em pt para reels"
}
$ alembic marketing discover req.json
marketing discover: C.D Advocacia
  brief: /tmp/cd-brief.json
  video: cinematic 9:16 (pt)
  provenance: offline-request

O parsing é determinístico e honesto: parseAsk aplica heurísticas em ordem ("shorts" → 9:16 antes de "youtube" → 16:9; "talking head"/"depoimento"/"cinematic"/"music"/etc → video.type; "sem fala"/"sem voz"/type === 'music'hasSpeech: false) e só seta o campo quando o texto pede inequivocamente. Tudo o que não foi dito permanece no default honesto do clientBriefSchemanunca fabricado. Os campos que o ask não tem como saber (industry, whatTheyDo, audience, competitors) ficam vazios; o provenance.source fica offline-request.

A trava 1: --online liga a research real

Passar --online wires o createBrightdataBriefResearch (a ACL sobre o CLI brightdata, em packages/marketing-factory/src/research.ts): o port faz scrape no site do cliente para extrair uma auto-descrição e discover para resultados rankeados sobre indústria/concorrentes/audiência. Toda field aprendida carrega um evidence { quote, url } de verdade — fields sem quote são omitidos, e uma rodada que não achou nada usável volta o no-op honesto { evidence: [] } (provenance fica em offline-request). Quando algo é aprendido, o mergeResearch sobrescreve só os defaults vazios com conteúdo não-vazio e flippa provenance.source = 'online-research'. Se o binário brightdata não está no PATH, o CLI devolve um err claro antes de qualquer chamada de rede — fail-closed.

marketing campaign — o capstone one-shot

runCampaign (em packages/marketing-factory/src/campaign.ts) compõe os três estágios: discoverClientBriefbriefToSignal + briefToFactoryOptionsrunMarketingFactorybriefToValidationPlancli.estimateCost. Tudo offline / $0 por default.

$ alembic marketing campaign req.json --out /tmp/cd-campaign.json
marketing campaign: C.D Advocacia (dry-run, $0)
  result: /tmp/cd-campaign.json
  video: cinematic 9:16 (pt)
  model: seedance_2_0  creatives: 1  kept: 0
  provenance: offline-request  dryRun: true
  validation plan: aspect 9:16, transcript true

Note as três coisas que o capstone entrega num único shot, sem gastar nada:

O BusinessSignal projetado

briefToSignal é uma função pura, total, sem clock/RNG: ela projeta o brief no BusinessSignal compartilhado para o motor de geração consumir o output da discovery sem mudar uma linha. As regras:

O par briefToSignal + briefToFactoryOptions é o que torna a factory multi-tenant by input: o brief carrega todo dado específico do cliente (marca, idioma, aspect, pronúncia falada, visuais proibidos, modelo de geração quando explícito), e o motor genérico atrás dele não sabe — nem precisa saber — quem é o cliente.

A trava 2: --approve --yes liga o higgsfield real

No CLI (apps/cli/src/commands.ts), a linha que decide é uma só:

// Paid generation needs BOTH --approve and --yes; a single flag stays offline.
const paid = args.approve && args.yes;
const cli = paid
  ? createHiggsfieldCli({ binary: 'higgsfield' })
  : fakeHiggsfieldCli(args.modelId ?? DEFAULT_MARKETING_MODEL_ID, args.soulId);

Uma flag sozinha nunca alcança o binário real. --approve sem --yes: fake cli, $0. --yes sem --approve: fake cli, $0. Os dois juntos: o createHiggsfieldCli de verdade é construído, e só aí o motor pode chamar generate create de verdade (que cobra créditos). O mesmo padrão de "AND-de-duas-flags" é o que marketing video (geração de cenas com Seedance, em ads.ts) usa: dryRun default true, a real generation precisa tanto de !dryRun quanto de approve: true.

A trava 3: validation plan vs. validation real

No campaign, validation é plan-only: briefToValidationPlan deriva os critérios que um QA real teria que aplicar (aspect, lang, checkTranscript, brand, acceptInitialism, forbiddenVisuals) diretamente do brief, sem nunca inventar um script ou required-phrases (o brief não carrega script). Isso é deliberado: num dry-run não há artifact real para validar. A validação real mora em validateVideo (em validate.ts) e roda só sobre um mp4 local — o caller precisa baixar o vídeo gerado primeiro. A gate cobre três dimensões independentes (aspectRatio derivado por ffprobe, transcript via whisper com brand-pronunciation + required phrases, frame OCR/vision para texto baked + forbidden visuals) — e cada uma é opt-in (omitir --script pula a dimensão transcript inteira, omitir --forbidden pula vision). Um vídeo speechless / music / com-texto passa por uma configuração diferente do mesmo gate.

A anti-corruption layer sobre o higgsfield

O HiggsfieldCli (interface em higgsfield.ts) é um typed wrapper sobre o CLI real higgsfield. Cada método shells para um subcomando, parsa o stdout --json, valida contra um schema RAW que mirrora exatamente o envelope do CLI, e então mapeia para um tipo DOMAIN limpo via um map* puro. Nenhum método joga — spawn failures, exit não-zero, JSON malformed e violações de schema todos colapsam para err. A interface é o seam: testes injetam um fake; produção usa createHiggsfieldCli com o subprocess real.

higgsfield generate create ... --json runJson(args, higgsfieldRawJobSchema) mapJob(raw) snake_case → domain HiggsfieldJob domain type snake_case + nulls + job_set_type + status "completed"/"in_progress"/... camelCase + 4 status normalizados + outputs[] com modality inferida a wire shape do CLI nunca escapa o módulo

O ganho da ACL é concreto: completed vira 'succeeded' (lá fora ninguém precisa saber que o CLI fala "completed"); job_set_type vira id; result_url .mp4 vira um outputs[] com modality: 'video' inferido; envelopes loose (uma dtc-ads list que pode vir como array puro ou como { items }) são tolerados defensivamente mas envelopes verdadeiramente malformed falham closed. Quando a Higgsfield mexer no shape do --json amanhã, só o RAW schema + o mapper mudam — o domain types (e portanto a factory inteira) permanece intacto. Esse é o ponto da ACL.

A factory: o coração de generate

runMarketingFactory (em flow.ts) é o motor. Pipeline para um signal validado:

  1. Carrega skills: loadMarketingSkills(skillsDir, ['product-marketing', 'copywriting', 'launch']) — markdown contexts; positioning é condicionado pelo product-marketing, copy pelo copywriting+launch.
  2. Constrói positioning + copy: com um CopyModel opcional, o modelo escreve; sem ele (ou se o modelo errar / output não parsear), degrada para o template determinístico, então uma rodada sempre entrega context+copy. skillsUsed é stamped da lista de skills carregadas, nunca do modelo — a procedência é honesta.
  3. Fan-out generation: o CLI real do higgsfield não tem --count (um generate create = um output), então a factory loopa o ciclo createGeneration → waitGeneration → score N vezes sequencialmente (CLI é credit-metered). Falha em qualquer passo é err imediato.
  4. Virality filter: cada output passa pelo ViralityScorer injetado. O default offlineViralityScorer é honesto: deriva um score pseudo-aleatório estável do hash da URL — não prediz virality, e o rationale diz isso ("offline heuristic; no live predictor"). Threshold default 0.5; um scorer real (Higgsfield MCP) é injetável sem mudar o flow.
  5. Assembly: assembleManifest hash-eia o body (signal + context + copy + creatives) com SHA-256; o manifestId = "mf-" + hash.slice(0, 16) e o contentHash vai no envelope — duas rodadas sobre inputs idênticos = mesmo hash. Content-addressable.

Brand-agnostic / multi-tenant na prática

C.D Advocacia é só o primeiro cliente. Não há código específico da C.D em parte alguma do @alembic/marketing-factory. O que muda entre clientes é o JSON do brief — marca (brand.spokenName, brand.acceptInitialism, brand.forbiddenVisuals), produto (business.whatTheyDo, business.audience, business.competitors), e o vídeo (video.type, video.aspect, video.language, video.hasSpeech, video.modelId opcional). A Appfy mesma usa esse mesmo surface para os próprios produtos. A factory roda any brand, any product, any video type.

O invariante spend-safe by construction é arquitetural, não doc. No flow.ts, o cli é um parâmetro injetado; a fábrica sozinha não sabe construir um cliente real. No campaign.ts, o runCampaign recebe o cli já decidido pelo caller. A única função em todo o monorepo que efetivamente constrói o createHiggsfieldCli({ binary: 'higgsfield' }) está no CLI (apps/cli/src/commands.ts) atrás do guard const paid = args.approve && args.yes. Quem importa o pacote como biblioteca recebe a mesma garantia: sem injetar um real cli, não há como gastar. A trava não é uma convenção; é a estrutura do código.

O dimensionamento opt-in do validate

$ alembic marketing validate video.mp4 \
    --script "Para sua aposentadoria…" --brand "Cê Dê Advocacia" \
    --aspect 9:16 --lang pt

As três dimensões do validateVideo são independentes e opt-in:

pass = AND(format ok, transcript ok, frames ok). Cada dimensão omitida é vácua (passa). É assim que o mesmo gate serve um cinematográfico falado, um ad musical, um teaser silencioso e um text-bearing card — sem ramos do tipo "if videoType === 'music' ..." dentro da validação.

Como funciona por dentro
  • flow.tsrunMarketingFactory (signal → manifest), assembleManifest (SHA-256 content-hash), runMarketingBatch (loopa signals, uma falha não aborta o batch). offlineViralityScorer é honest-named — não prediz virality; o rationale diz "offline heuristic; no live predictor".
  • discover.tsparseAsk (heurísticas ordenadas; "shorts" antes de "youtube"; type === 'music' implica hasSpeech: false), discoverClientBrief (offline skeleton → optional research merge → re-valida), mergeResearch (só flippa provenance.source = 'online-research' quando algo foi de fato aprendido — um no-op research port não fabricates uma online provenance), briefToSignal (puro, total, sem clock/RNG).
  • brief-options.tsbriefToFactoryOptions: video.modelId explícito vence; type === 'text'gpt_image_2 (testado antes do speech default, porque hasSpeech tem default true e um text-card não pode cair no video model); hasSpeech || type ∈ {talking-head, cinematic, product, ugc, explainer, music}seedance_2_0; senão undefined.
  • campaign.tsrunCampaign orquestra os 3 estágios + cost preview best-effort; briefToValidationPlan deriva o plan puro do brief (sem inventar script/required-phrases); dryRun default true; estimateRunCost falhar é WARN-and-omit (não err).
  • higgsfield.ts — a ACL: runJson (spawn → JSON → schema → Result); RAW schemas (higgsfieldRawJobSchema, higgsfieldRawCostSchema, etc — mirroram o envelope do CLI verbatim); pure mappers (mapJob, mapCost, mapModel, ...). O createHiggsfieldCli retorna a interface; fakeHiggsfieldCli (em marketing-seams.ts do CLI app) implementa a mesma interface offline.
  • validate.tsvalidateVideo com 3 dimensões opt-in; Transcriber / FrameOcr / FrameDescriber são seams injetáveis (o pacote não puxa @alembic/ocr ou @alembic/vision direto — quem wires é o CLI).
  • ads.tsgenerateAdScenes (Stage 3 do ads-factory): estimateCost FIRST, depois — só com !dryRun && approvecreateGeneration → waitGeneration → optional QA. Mesmo padrão AND-de-duas-flags.
  • research.tscreateBrightdataBriefResearch: ACL sobre o CLI brightdata; schemas defensivos .passthrough() (a shape exata só é confirmada na rodada real founder-gated); nothing-learned → { evidence: [] }; os comandos reais do brightdata CUSTAM dinheiro, então o pacote é exercitado offline com um fake CliRunner.

Verifique seu entendimento

Por que parseAsk testa "shorts" antes de "youtube" e como isso se conecta ao princípio anti-fabricação?

Porque "YouTube Shorts" é um vertical 9:16, mas a regra "youtube" sozinha mapearia para 16:9 (landscape). A ordem garante que o ask mais específico vence sobre o ask mais genérico. O princípio é o mesmo de toda a discovery: só seta o campo quando o texto pede inequivocamente. Um campo que o ask não consegue ler com confiança fica em default honesto do schema — nunca chutado.

Você passa --approve mas esquece --yes num marketing campaign. O que acontece?

Zero spend. A linha decisória é const paid = args.approve && args.yes; — uma flag sozinha avalia para false, e o cli fica sendo o fakeHiggsfieldCli (preview $0). O brief é aprendido (offline, porque --online não foi passado também), o manifest é gerado contra o fake cli, o validation plan é derivado, e o output diz "(dry-run, $0)". A trava não é uma checagem em runtime que poderia falhar — é uma construção condicional: o real CLI só é instanciado quando os dois flags são true.

Por que o pacote tem schemas RAW separados dos types domain, em vez de um schema só?

Por causa da ACL. Os RAW schemas (higgsfieldRawJobSchema, brightdataRawScrapeSchema, etc) mirroram exatamente a wire shape do CLI externo — snake_case, job_set_type, completed/in_progress, envelopes union (bare-array OR { items }), .passthrough() defensivo. Os types domain (HiggsfieldJob, BriefResearch) são limpos — camelCase, 4 status normalizados, outputs[] com modality inferida. O map* puro fica no meio. Resultado: quando o vendor mexer no shape do --json, só RAW+mapper mudam; o resto do código (e os consumidores do pacote) não vêem o tremor. É o ganho clássico de uma anti-corruption layer.

Próximas paradas: ai-employee (a camada de composição que pode rodar a factory como tarefa agendada), factory-forge-course (o factory genérico — @alembic/factory + forge — de onde a marketing factory herda o padrão spend-safe), use-cases (o walkthrough end-to-end de C.D Advocacia: marketing discovercampaign → review).