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
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 clientBriefSchema — nunca 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: discoverClientBrief → briefToSignal + briefToFactoryOptions → runMarketingFactory → briefToValidationPlan → cli.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
briefaprendido (offline-request, porque--onlinenão foi passado). - O
manifestde assets viafakeHiggsfieldCli(preview $0 —creatives: 1,kept: 0porque o offline scorer é heurístico determinístico, não um predictor real). - O
validationPlan— a planilha de critérios que um QA real iria aplicar:aspect: '9:16'(do brief),checkTranscript: true(o vídeo tem fala),forbiddenVisuals: [](o brief não declarou nenhum). É o plano, não o resultado — não existe mp4 ainda.
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:
id = "cb-" + slug(client.name)— determinístico.kind= mapa dobusiness.reason:awareness|launch → 'trend';conversion|retention → 'validation';recruiting → 'gap';education → 'tech'.evidenceQuote= primeiro não-vazio de[provenance.evidence[0].quote, client.oneLiner, business.whatTheyDo, "{name} — {type} video"].strength=3 + (evidence? 1: 0) + (whatTheyDo? 1: 0), clamped em [1, 5].confidence=0.5 + (online-research? 0.2: 0) + (competitors? 0.1: 0), clamped em [0, 1].
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.
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:
- Carrega skills:
loadMarketingSkills(skillsDir, ['product-marketing', 'copywriting', 'launch'])— markdown contexts; positioning é condicionado peloproduct-marketing, copy pelocopywriting+launch. - Constrói positioning + copy: com um
CopyModelopcional, 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. - Fan-out generation: o CLI real do
higgsfieldnão tem--count(umgenerate create= um output), então a factory loopa o ciclocreateGeneration → waitGeneration → scoreN vezes sequencialmente (CLI é credit-metered). Falha em qualquer passo éerrimediato. - Virality filter: cada output passa pelo
ViralityScorerinjetado. O defaultofflineViralityScoreré 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 default0.5; um scorer real (Higgsfield MCP) é injetável sem mudar o flow. - Assembly:
assembleManifesthash-eia o body (signal + context + copy + creatives) com SHA-256; omanifestId="mf-" + hash.slice(0, 16)e ocontentHashvai 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.
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:
--scriptliga a dimensão transcript: extrai o áudio comffmpeg, transcreve comwhisper(via oCliRunnerseam), checaminScriptSimilarity(default 0.6) +requiredPhrases. Omitir é declarar que o vídeo é speechless (música, b-roll, UGC montage) — a dimensão toda é skippada.--brand "Cê Dê Advocacia"liga oBrandPronunciationcheck sobre a transcript (spokenform).--accept-initialismpermite que um clean initialism como "CD" passe com a caveat "verify the accent by ear".--forbiddenliga o vision scan (frames extraídos viaffmpeg→ oFrameDescriberseam, wired pelo CLI para@alembic/vision).--no-textliga o OCR scan (frames → oFrameOcrseam, wired para@alembic/ocr) — útil para um ad de pura imagem sem texto baked, em contraste com um card text-bearing onde você quer texto na frame.
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.ts—runMarketingFactory(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.ts—parseAsk(heurísticas ordenadas; "shorts" antes de "youtube";type === 'music'implicahasSpeech: false),discoverClientBrief(offline skeleton → optional research merge → re-valida),mergeResearch(só flippaprovenance.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.ts—briefToFactoryOptions:video.modelIdexplícito vence;type === 'text'→gpt_image_2(testado antes do speech default, porquehasSpeechtem defaulttruee 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.ts—runCampaignorquestra os 3 estágios + cost preview best-effort;briefToValidationPlanderiva o plan puro do brief (sem inventar script/required-phrases);dryRundefaulttrue;estimateRunCostfalhar é WARN-and-omit (nãoerr).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, ...). OcreateHiggsfieldCliretorna a interface;fakeHiggsfieldCli(emmarketing-seams.tsdo CLI app) implementa a mesma interface offline.validate.ts—validateVideocom 3 dimensões opt-in;Transcriber/FrameOcr/FrameDescribersão seams injetáveis (o pacote não puxa@alembic/ocrou@alembic/visiondireto — quem wires é o CLI).ads.ts—generateAdScenes(Stage 3 do ads-factory): estimateCost FIRST, depois — só com!dryRun && approve—createGeneration → waitGeneration → optional QA. Mesmo padrão AND-de-duas-flags.research.ts—createBrightdataBriefResearch: ACL sobre o CLIbrightdata; 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 fakeCliRunner.
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 discover → campaign → review).