name: generate-grid description: 'Grid editorial mensal por marca da InsideOut — gerar grid do mês a partir do briefing, ingerir planilha histórica (2026), mover/trocar/editar posts e abrir o grid HTML. Use para "gera o grid de maio da Clinique do briefing", "cria o grid de maio da Clinique", "ingere essa planilha", "move o post do dia 6 pro dia 8", "troca os posts do dia 3 e 5", "abre o grid", "edita o post do dia 10".' allowed-tools: Bash, Read, Write argument-hint: '[gerar do briefing | criar grid vazio | ingerir planilha | mover/trocar post | editar post | abrir grid]' disable-model-invocation: false
Generate Grid — grid editorial mensal por marca InsideOut
Terceiro pilar da geração de social media. A style-gallery diz "como a
peça parece"; o product-catalog diz "o que é o produto e como a marca
fala"; aqui mora "o que postar e quando": um grid = 1 marca × 1 mês,
colapsando num único artefato canônico as duas planilhas Excel que a Estela
(social media Clinique/EL/TF) mantém hoje à mão.
Tom com o usuário (sempre): quem opera não é técnico. Leia e aplique
${CLAUDE_PLUGIN_ROOT}/skills/voz-usuario.md— fale de grid, post, dia, marca; nunca de implementação (HTML, JSON, campo, caminho, "renderizar", encoding). Resolva erros nos bastidores e relate só o essencial.
Escopo desta versão (Fase 3): esqueleto canônico + geração do grid a partir do briefing (Fase 2) + mockup por post via Gemini 3 Pro (Fase 3) + ingestão do histórico 2026 + edição conversacional + grid HTML navegável. As regras da Estela (
rules/<marca>.md) e o calendário comemorativo (calendar/<ano>.md) são editáveis em Markdown e consumidos na geração — o julgamento é seu guiado por elas, o código só cuida do mecânico. Validação visual dos mockups é da Estela — sem gate automático (image gen é não-determinístico).
Onde rodar (crítico)
O diretório do plugin (${CLAUDE_PLUGIN_ROOT}) é read-only e efêmero por
sessão no Cowork. O grid vivo do cliente vive na pasta de trabalho,
não no plugin. Nunca faça cd para o core/; importe via sys.path com
cwd = pasta de trabalho.
Padrão de invocação (use em tudo abaixo):
CORE="${CLAUDE_PLUGIN_ROOT}/core"
python -c "
import sys; sys.path.insert(0, r'$CORE')
try: sys.stdout.reconfigure(encoding='utf-8')
except Exception: pass
import grid_library as gl
# ... chamada ...
"
Dependências (se faltar import): pip install -r "$CORE/requirements.txt"
(a ingestão de planilha precisa de openpyxl).
Onde fica o grid
gl.find_library_dir() resolve nesta ordem:
- variável de ambiente
GRIDS_DIR, se setada; - busca pra cima a partir do cwd por uma pasta
grids/existente (para na raiz do git/filesystem) — rodar de uma subpasta não duplica; - se nada: cria
<pasta de trabalho>/grids/.
Estrutura: grids/<marca>/<AAAA-MM>.json (1 arquivo por grid marca-mês),
grids/rules/<marca>.md (regras editáveis da marca), grids/calendar/<ano>.md
(calendário comemorativo compartilhado), grids/mockups/<AAAA-MM>/<dia>.{png,json}
(mockup IA por post + sidecar JSON de auditoria), grids/.trash/. Sem grid no
workspace, leitura cai no seed embarcado (1 grid Clinique exemplo) — funciona
com zero config. A visualização HTML é o painel unificado (insideout-painel.html,
gerado na raiz da pasta de trabalho, aba Grid) — ver "Abrir o grid".
Se for um repositório git, garanta no .gitignore da pasta de trabalho:
ignorar insideout-painel.html (derivado) e grids/.trash/; versionar
grids/<marca>/*.json, grids/rules/*.md, grids/calendar/*.md e
grids/mockups/ (é o ativo editorial do cliente).
Operações
Primeiro uso é automático — não chame bootstrap manualmente. Qualquer
operação que exibe/cria/muta já faz lazy-ensure: materializa o seed
(grid exemplo + rules/<marca>.md + calendar/<ano>.md) no workspace,
idempotente, nunca sobrescrevendo o que existe.
Listar / ver:
for g in gl.list_grids(): print(g['brand'], g['month'], g['posts'])
for g in gl.list_grids('clinique'): print(g['month'])
print(gl.get_grid('clinique', '2026-05')) # grid completo
Criar grid vazio do mês (esqueleto semanal domingo→sábado, só os dias do mês; preenche depois conversando):
gl.new_grid("clinique", "2026-05",
focus_products=["almost-lipstick-black-honey"])
# ou: gl.new_grid("clinique", "maio", year=2026)
Ingerir planilha histórica (só 2026 — Fase 1):
gl.xlsx_sheets("/caminho/PLANILHA.xlsx") # lista as abas
gl.ingest_xlsx("/caminho/ESTRATEGIA.xlsx",
sheet="CL_MAIO", brand="clinique", month="05", year=2026)
gl.ingest_xlsx("/caminho/BRIEFING DESIGN.xlsx",
sheet="CLINIQUE - MAIO 2026", brand="clinique", month="05")
sheet, brand e month são explícitos, mas não confiados: a aba
tem que provar ser {mês}/2026 ou é recusada (nunca regravada com ano/mês
errado). A prova é determinística: ano no nome da aba quando existe (Briefing
Design) + âncora de calendário (coluna do dia-1 e nº de dias do mês). Nome e
célula-título da aba mentem (vimos CL JANEIRO com título NOVEMBRO, e
CL MAIO que é maio/2025); o layout dos dias não mente. Detecta o tipo
sozinho: linha ABORDAGEM → Estratégia Mensal; STORY → Briefing Design.
⚠️ Rode a ingestão SEMPRE in-process (este bloco Python único), nunca
por python -c cujo stdout é capturado: o console Windows (cp1252) corrompe
acento e grava � silencioso. Há guard que recusa U+FFFD, mas a regra é não
expor a ingestão ao round-trip de console.
Gerar do briefing (Fase 2) — andaime mecânico + loop de julgamento:
A geração é híbrida de propósito: o código (grid_library) faz o
mecânico — esqueleto do mês, ancora launches, casa datas comemorativas,
propõe intercalação, garante invariantes de cadência (gap≤2, ≥28 posts) — e
marca cada dia com um _slot tipado (launch_anchor, launch_intensity,
calendar_hook, focus_intercalation, hero_fill, free). Você faz o
julgamento — qual produto na data, hero, ref de estilo, spoiler-ou-não —
guiado pelas regras editáveis em grids/rules/<marca>.md e materializa cada
decisão via set_post(..., rationale=<porquê>). O código não chama LLM nem
decide conteúdo; o conteúdo do dia (product/subject/ref/lettering)
sai vazio do andaime — é você que preenche.
O input é o dict brief (boundary object com analyze-briefing — Passo 5
de lá emite esse dict; também pode vir de conversa direta):
brief = {
"brand": "clinique", "month": "2026-05",
"launches": [{"date": "2026-05-04", "product": "<slug>",
"label": "...", "important": True}],
"focusProducts": ["<slug>", "..."],
"globalContent": [{"date": None, "note": "campanha Global semana 3"}], # opc.
"directionalNotes": "...", # opc.
}
v = gl._validate_brief(brief) # falha-alto em brand/month; reporta slug fantasma em v['missing']
print('missing:', v['missing'])
g = gl.generate_from_briefing(v['brief']) # andaime + _slot por dia; persiste e regera HTML
pc = gl.plan_card(g) # dossiê: rules/<marca>.md inline + slots a decidir
Se o grid generate_from_briefing
recusa por segurança (não obliterar curadoria). Pergunte ao usuário antes
de passar overwrite=True — é destrutivo.
⚠️
generate_from_briefingsozinho NÃO é um grid pronto. Ele só monta o andaime com os slots vazios — apresentar esse calendário em branco ao usuário é o bug nº 1 relatado pela Carol. O loop de julgamento abaixo é obrigatório e contínuo: quando o usuário pede "gera o grid", rode o loop na mesma sequência atégl.plan_card(g)['slotsTodo']esvaziar, depoisaudit_grid, e só então apresente. Nunca pare no andaime nem diga "grid criado" com os dias vazios. Sem mockups no primeiro take (vêm num passo posterior). Vale aqui e quando aanalyze-briefingengatilha a geração.
Loop de julgamento (você, não código): itere pc['slotsTodo'] (lista de dias
ainda sem subject/product). Para cada slot:
- Leia o plan-card:
pc['rules']['text']é o texto integral degrids/rules/<marca>.md— leia inteiro a cada lote (a Estela edita).pc['launches']+pc['focusProducts']+pc['calendarMonthWide']dão o panorama.slot['_slot']trazkind,hint,product(se proposto pela âncora) ecalendarHook(se a data tem gancho comemorativo). - Decida
product/subject/ref/channel/lettering/spoilerconsultando o que precisar:pc.list_products(emproduct-catalog),pc.list_styles(emstyle-gallery). - Materialize com
gl.set_post(..., rationale="<por que essa escolha>"). Sempre preencha orationale— é o log de aprendizado pedido pelo Lucas. Em slotslaunch_anchor(locked), respeite oproductproposto. - A cada lote (5-10 slots), rode
gl.audit_grid(g['brand'], g['month'])e recomputegl.plan_card(gl.get_grid(...))— o slot decidido some deslotsTodoe os warnings se atualizam. - Antes de apresentar o grid pronto:
audit_gridzerado dewarningsé o sinal de que o mecânico fechou. Julgamento (qual produto era certo na data) é revisão da Estela, não checagem automática.
# loop pragmático
g = gl.generate_from_briefing(v['brief'])
while True:
pc = gl.plan_card(g)
if not pc['slotsTodo']:
break
s = pc['slotsTodo'][0]
# ... decida product/subject/ref/etc. lendo pc['rules']['text'] ...
gl.set_post(g['brand'], g['month'], s['date'],
product='<slug>', subject='<...>', approach='<...>',
channel='<feed|story|reels|carrossel>',
ref={'kind':'style','id':<N>},
rationale='<por que essa escolha>')
g = gl.get_grid(g['brand'], g['month'])
print(gl.audit_grid(g)) # warnings vazios = mecânico fechou
print(gl.open_grids())
Em edição manual (set_post/move_post/swap_posts depois do grid
pronto): rode audit_grid e avise o usuário se a edição quebrou regra
codificável (gap>2, foco sumiu) — mas não bloqueie. A Estela é a dona;
o aviso é dela pra ela.
Editar posts (reescreve o JSON canônico atomicamente — NUNCA o HTML):
gl.move_post("clinique", "2026-05", "2026-05-10", "2026-05-11") # puxa pra outro dia
gl.swap_posts("clinique", "2026-05", "2026-05-03", "2026-05-05") # troca dois dias
gl.set_post("clinique", "2026-05", "2026-05-06",
product="almost-lipstick-black-honey", approach="PRODUTO",
subject="Black Honey", channel="feed",
ref={"kind": "style", "id": 3},
lettering={"topo": "O tom que vira a sua cor"},
rationale="Por que essa escolha — vira log de aprendizado")
gl.clear_post("clinique", "2026-05", "2026-05-06") # esvazia o slot
Campos editáveis de um post: channel, approach, product, subject, ref, lettering, copy, mockup, video, rationale, notes (data e dia-da-semana são
imutáveis). mockup é a imagem do dia; video é o vídeo do dia (mp4) — o card
mostra o vídeo como pôster + ▶ quando há.
copy é a legenda do post (Hook→Valor→CTA, vinda da generate-copy) — é o
que aparece no rodapé do card no painel; subject é só o assunto curto, notes
não aparece no painel. Legenda com acento entra in-process (a set_post
recusa texto corrompido por console/python -c cp1252).
approach segue a taxonomia da planilha: LANÇAMENTO, FARMA,
EDUCACIONAL, DATA OPORTUNIDADE, PRODUTO, TREND. product é o slug do
product-catalog quando há produto; null em data-oportunidade (use
subject). ref = {"kind":"style","id":N} (estilo curado da
style-gallery, caminho preferido) ou {"kind":"url","url":"..."}.
Regras e calendário (curadoria humana — arquivos Markdown):
grids/rules/<marca>.md— as regras da Estela pra montar o grid daquela marca, em linguagem humana. Materializado do seed; a Estela/Carol editam direto (é a fonte do julgamento da Fase 2, não código).grids/calendar/<ano>.md— datas comemorativas, compartilhado entre marcas. Tabela Markdown editável.Quando o usuário pedir pra "ajustar as regras" / "adicionar uma data comemorativa": leia o arquivo, mostre, edite com
Write/Edite confirme. Não embuta regra/data em código.
Abrir o grid:
print(gl.open_grids()) # regenera o painel unificado e devolve o caminho (abre na aba Grid)
O grid virou a aba Grid do painel único da InsideOut (insideout-painel.html,
na raiz da pasta de trabalho). Informe o caminho ao usuário e diga para abrir no
navegador — o painel abre direto na aba Grid (deep-link #grid). Seletor de marca
global no topo + sub-filtro de mês na aba; cada dia mostra abordagem (cor),
subject/produto, canal, lettering e mockup (quando houver).
Mockup por post (Fase 3) — orquestração de Gemini 3 Pro Image:
A geração de mockup é híbrida: o código (grid_library) faz o mecânico
(monta brief via compose_generation_brief em ref+product/product_only,
ou prompt direto em ref_only; move PNG; escreve sidecar JSON; atualiza
post.mockup); você faz o enriquecimento do prompt
(iluminação/mood/atmosfera/composição — regra herdada da skill
image-generation) entre o dry-run e a chamada real.
Pré-condições:
.envna pasta de trabalho comGEMINI_API_KEY(mesmo padrão daimage-generation; o agente cria/gerencia).- Pelo menos
productOUrefpreenchido no post —set_postantes se necessário.
Padrão dry-run-first (regra forte, não opcional) — agente pré-visualiza o brief + custo antes de queimar tokens:
# 1) preview: monta brief sem chamar Gemini, custo estimado, $0
out = gl.mockup_for_post("clinique", "2026-05", "2026-05-10", dry_run=True)
print(out["brief"]["prompt"][:400])
print("custo estimado:", out["cost_estimate_usd"], "USD")
# 2) enriqueça out["brief"]["prompt"] com iluminação/mood/composição
enriched = out["brief"]["prompt"] + (
"\n\n[ENRIQUECIMENTO DO AGENTE]: iluminação difusa de manhã, "
"fundo neutro com textura sutil, composição centralizada com 30% "
"de área livre superior pra lettering, mood acolhedor."
)
# 3) geração real (consome ~$0.10 / 1K)
final = gl.mockup_for_post("clinique", "2026-05", "2026-05-10",
prompt_override=enriched)
print("mockup em:", final["mockup"])
Defaults: compose_mode="preservar" (packaging real intocado);
resolution="1K"; aspect_ratio deriva de channel (story/reels→9:16;
feed/carrossel→1:1). Override por kwarg quando necessário.
Refinamento conversacional (Estela: "mais clean", "mais ar"):
gl.mockup_for_post("clinique", "2026-05", "2026-05-10",
continue_session=True,
prompt_override="ajuste: mais clean, menos cluttered")
continue_session=True mantém o histórico multi-turn do Gemini só pra
esse refinamento. Default isola (new_session() automático antes da call).
Batch (gerar todos os mockups que faltam):
# 1) dry_run primeiro pra ver custo agregado
out = gl.batch_mockups("clinique", "2026-05",
only_empty=True, dry_run=True)
print(f"{len(out['generated'])} mockups a gerar, "
f"~${out['total_cost_estimate_usd']} total. "
f"{len(out['skipped'])} skip: {out['skipped'][:3]}...")
# 2) CONFIRMAR custo com o usuário antes do real
# 3) real
out = gl.batch_mockups("clinique", "2026-05", only_empty=True)
Sessão Gemini isolada entre cada post (sem cross-contamination).
Sidecar JSON (mockups/<AAAA-MM>/<dia>.json): cada mockup grava brief
completo + metadados (kind, compose_mode, resolution, custo,
generated_at, model, flag prompt_was_overridden). Versionável,
auditável, conta a história da peça.
Limites conhecidos:
- URL refs (
ref.kind="url"): 1 tentativa comUser-Agent: Mozilla/5.0, timeout 10s. Hosts JS-render (Pinterest moderno) falham — recusa-fofa empurra pro caminho canônico (cadastrar o estilo emstyle-gallery). _download_url_refnão valida que o conteúdo baixado é imagem; URL que retorna HTML vira "arquivo" e o Gemini falhará ao ler. Prefira semprestyle-galleryquando possível.- Image gen é não-determinístico — não há gate automático de qualidade. Validação é visual, pela Estela.
Vídeo por post (Veo) — grid-nativo, gera-e-anexa:
Para vídeo (stories/reels), o caminho é video_for_post — espelha o mockup:
dry-run-first, o agente enriquece o movimento, e ele anexa o vídeo ao
dia sozinho. Usa o mockup (imagem) do próprio post como frame-âncora
(image-to-video) quando existir, pra consistência de cena.
# 1) dry-run: monta o prompt-base + nota de custo, $0, sem gerar
out = gl.video_for_post("clinique", "2026-05", "2026-05-11", dry_run=True)
print(out["brief"]["prompt"][:400]); print(out["cost_note"])
# 2) enriqueça out["brief"]["prompt"] com MOVIMENTO de câmera, ação e mood
enriched = out["brief"]["prompt"] + "\n\n[MOVIMENTO]: zoom-in lento no produto, ..."
# 3) CONFIRME custo/tempo com o usuário (vídeo é caro e lento) e gere
final = gl.video_for_post("clinique", "2026-05", "2026-05-11",
prompt_override=enriched, aspect_ratio="9:16")
print("vídeo em:", final["video"])
Default aspect_ratio="9:16" (stories/reels; override 16:9 p/ horizontal).
O vídeo aparece no card como pôster (1º frame) + ▶; clique abre e toca.
Vídeo ad-hoc já pronto (gerado solto, baixado) → gl.attach_video(marca, mês, dia, <mp4>). Pré-condição: .env com GEMINI_API_KEY (igual generate-video).
Relação com as outras skills
analyze-briefingPasso 5 emite o dictbrief(boundary object) que esta skill consome emgenerate_from_briefing. A comunicação é só via esse dict — sem estado compartilhado.product-catalogé a fonte do campoproduct(slug)._validate_briefreporta slugs fantasma; cadastre o produto lá antes de referenciá-lo no grid (ou ajuste o slug do briefing).style-galleryé a biblioteca de refs:ref.kind="style"aponta pra um estilo curado lá. A Fase 3 consomestyle-galleryem todos os modos de mockup com ref.image-generationé o motor que a Fase 3 orquestra (image_gen.generatevia boundary lazy import). A skillimage-generationmesmo continua sendo pra criação ad-hoc fora do contexto de grid;generate-gridFase 3 usa o mesmo motor pra mockup-por-post com sidecar de auditoria.
Lógica de decisão
- "gera o grid do mês a partir do briefing" →
_validate_brief→generate_from_briefing→plan_card→ loop de julgamento (set_post por slot, lendorules/<marca>.md) →audit_gridantes de apresentar. Se o briefing veio da skillanalyze-briefing, o dictbriefjá chega pronto do Passo 5 de lá. - "cria/monta o grid de <mês> da
" (sem briefing) → new_grid(esqueleto vazio do mês). Útil pra começar manual; ofereça também o caminho do briefing. - "ingere/importa essa planilha", "puxa o histórico" →
xlsx_sheetspra achar a aba, confirme marca/mês,ingest_xlsx(só 2026). - "move/puxa o post do dia X pro dia Y" →
move_post. - "troca os posts do dia X e Y" →
swap_posts. - "muda/edita o post do dia X" (produto, abordagem, legenda, canal…) →
set_post. Após editar, considereaudit_gridpra avisar se quebrou regra codificável (gap>2, foco sumiu). - "tira/esvazia o post do dia X" →
clear_post(confirme). - "apaga o grid de <mês>" → confirme explicitamente;
delete_grid(reversível via.trash/). - "que grids eu tenho", "abre o grid" →
list_grids/open_grids. - "ajusta as regras / adiciona data comemorativa" → editar
grids/rules/<marca>.mdougrids/calendar/<ano>.mdcom Read+Edit. - "gera o mockup do dia X" → dry-run primeiro (
mockup_for_post(..., dry_run=True)) → enriqueça o prompt → real (prompt_override=...). Reporte o caminho do PNG e do sidecar JSON. - "regenera o do dia X mais clean / com mais ar / outra abordagem" →
mockup_for_post(..., continue_session=True, prompt_override="<ajuste>")no mesmo post. - "gera todos os mockups que faltam" →
batch_mockups(only_empty=True, dry_run=True)primeiro → confirme custo agregado com o usuário →batch_mockups(only_empty=True)real. - "muda o estilo do dia X pra #N e regenera o mockup" →
set_post(ref= {"kind":"style","id":N})+mockup_for_post. - "gera o vídeo do dia X" / "faz um story em vídeo pro dia X" → dry-run
primeiro (
video_for_post(..., dry_run=True)) → enriqueça com movimento → confirme custo/tempo → real (prompt_override=...). Usa o mockup do dia como frame-âncora se houver. NÃO use agenerate-videostandalone pra isso — ovideo_for_posté que anexa ao dia sozinho. - "põe esse vídeo (que já tenho) no dia X" →
attach_video(marca, mês, dia, <mp4>).
Regras importantes
- Confirme antes de deletar/esvaziar; nunca delete em lote.
- Nunca edite
insideout-painel.html(derivado) nem os*.jsonna mão — use as funções (escrita atômica + regen do grid). A planilha apodrecia exatamente por edição manual. core/é read-only: nunca grave lá; toda escrita vai pra pasta de trabalho.- Ao editar um post, preencha o
rationale(1 linha do porquê) quando o usuário der a razão — é o log de aprendizado pedido pelo Lucas. - Legenda (copy) vai no campo
copyviaset_post(copy=...)— nunca nosubject/notes. Texto com acento é escrito in-process (aset_postrecusa mojibake de console/python -ccp1252). - Arte/vídeo PARA um dia do grid → use sempre o caminho grid-nativo que
gera-e-anexa atômico:
mockup_for_post(imagem) ouvideo_for_post(vídeo). Esse caminho insere no dia sozinho — é o que evita o bug de "gerou mas não entrou no grid". - Mídia ad-hoc (criada solta na
image-generation/generate-video, print, download) que vai pro grid → anexar é passo obrigatório:attach_mockup(marca, mês, dia, <imagem>)ouattach_video(marca, mês, dia, <mp4>). Nuncaset_post(mockup=/video=)com caminho cru (sumia/duplicavagrids/grids/...), e nunca reportar "pronto" deixando a mídia só emoutputs/quando o usuário a queria no grid. - Sempre reporte o caminho do painel (
insideout-painel.html) ao abrir/atualizar. - Não exponha caminhos de arquivo a menos que o usuário peça — fale em "marca/mês" e datas.
- Fase 3 —
.envcomGEMINI_API_KEYna pasta de trabalho (mesma regra deimage-generation; o agente cria/gerencia). Sem chave, a primeira chamada real estouraValueError "GEMINI_API_KEY not found". - Fase 3 — dry-run-first é não-negociável. Toda primeira invocação
de
mockup_for_post(dry_run=False)oubatch_mockups(dry_run=False)sai com dry-run antes, prompt enriquecido pelo agente, custo confirmado com o usuário. Sem atalho. - Fase 3 — overwrite intencional. Regenerar mockup sobrescreve PNG e sidecar JSON. Auditoria histórica vive no git do workspace, não em versionamento manual de arquivos.
Tratamento de erros
GridNotFound: confira comlist_grids(); o seed é materializado no 1º uso (lazy-ensure) e é editável como qualquer registro.InvalidGrid: mês fora deAAAA-MM, grid sem brand/month, ou campo de post não editável — corrija conforme a mensagem.GridError"Ingestão limitada a 2026" / "tem ano … no nome" / "corresponde a [outros anos]" / "vai até o dia N, mas … tem M dias": a aba não provou ser {mês}/2026 (é 2025, ano no nome, ou mês trocado). Recusa proposital — não é bug. Escolha a aba 2026 certa (xlsx_sheets()); na Estratégia o redo 2026 costuma ser a variante com underscore (CL_MAIO).GridError"contém U+FFFD (mojibake)": a ingestão foi conduzida por console/python -ccapturado (cp1252 corrompeu acento). Rode in-process.GridError"openpyxl ausente":pip install -r "$CORE/requirements.txt".GridError"plugin mal empacotado":grids.seed.json/rules-seed/calendar-seedausentes ou inacessíveis nocore/instalado — reinstalar/atualizar o plugin. (Falha alto de propósito: mesma disciplina do bug UWP 0.3.7.)- Import falha:
pip install -r "$CORE/requirements.txt". GridError"Post X sem product nem ref" (Fase 3) — preencha um dos dois viaset_postantes de gerar mockup. Slots vazios não viram imagem.GridError"Não consegui baixar a ref URL" (Fase 3) — host bloqueou ou caiu. Cadastre o estilo emstyle-gallery(caminho canônico) ou aponte o ref pra um id de estilo curado lá.GridError"image_gen.generate não retornou caminho" (Fase 3) — verifiqueGEMINI_API_KEY, quota da conta, filtro de conteúdo (NSFW), e o tamanho do prompt enriquecido.ValueError "GEMINI_API_KEY not found"(Fase 3, vem doimage_gen) — crie.envna pasta de trabalho com a chave do https://aistudio.google.com/apikey. Idem padrão daimage-generation.ProductCatalogError "modo inválido"(Fase 3) —compose_modesó aceita"preservar"(default) ou"recriar".