Integração fal.ai — Arquitetura Agnóstica de Mídia
Guia operacional e de integração com a API da fal.ai para geração e edição de imagens de forma assíncrona.
Status (2026-07-16): documento operacional validado contra o código atual. Hoje existem capacidades de remoção de fundo e profiles de geração de imagem, executados pela camada genérica de mídia.
image.remove-backgroundjá está executada pela camada genéricasrc/lib/media/(catálogo, profile, runner e transporte fal.ai). Jobs assíncronos e as demais capacidades continuam como direção arquitetural, marcadas na seção 8.
1. Decisão arquitetural
fal.ai é um backend de execução de mídia por capacidades, separado do
AIProvider de chat.
- O
AIProvider(src/lib/ai/providers/) modela conversa: streaming de texto, tool-calling, usage de tokens, um turno síncrono. Versrc/lib/ai/SPEC.md. - Tarefas de mídia (remover fundo, upscale, outpaint, vídeo…) têm ciclo de
vida diferente: entrada = uma imagem/asset, execução potencialmente longa
(job), saída = um binário re-hospedado, custo por chamada, sem tokens nem
streaming. Forçar isso dentro do
AIProviderde chat vazaria conceitos.
Por isso a mídia vive em src/lib/media/images/ (utilitários) + endpoints próprios em
src/pages/api/ai/image/, e o fal.ai entra como um provider entre outros
atrás de um contrato pequeno — do mesmo jeito que o Pix é agnóstico de PSP
(PixProvider) e o chat é agnóstico de vendor (AIProvider).
Invariante: o vendor não participa das decisões do domínio nem é enviado
como comando pelo cliente. A comparação da UI mostra apenas as artes; a
proveniência (Gemini, Fal / Recraft, Fal / Ideogram) permanece nos
metadados administrativos. Trocar o executor continua restrito ao catálogo e à
policy server-side.
2. Fluxo
UI / domínio server externo
──────────── ────── ───────
ChatInterface (P2 auto-bg) ┐
ImageViewer ("Remover fundo")├─▶ background-removal-flow.ts
┘ │ (orquestra: server → fallback client)
├─▶ POST /api/ai/image/remove-bg ──┐
│ pickServerBgProvider() │
│ └─ createFalProvider │
│ └─ runMediaCapability │
│ ├─ catalog/profile │
│ └─ fal transport ──────┼─▶ fal.run
│ persistAlphaPng() ────────────┼─▶ Supabase Storage
│ │ user-assets/ai-images
└─▶ (fallback) removeImageBackground (imgly, client)
+ POST /api/ai/image/process ─▶ persistAlphaPng
background-removal-flow.ts(client) é o orquestrador: tenta o servidor primeiro; em503/502/timeout cai para o modelo client-side (imgly) + persist./api/ai/image/remove-bg(server) resolve o provider e persiste o PNG alpha.- Persistência sempre via
persistAlphaPng→ Supabase Storage + linha emai_generated_images.
A seleção do backend de remoção continua em
pickServerBgProvider(). Dentro do provider Fal,runMediaCapability()resolve o profile e o runner. A policy dinâmica por custo/qualidade e os jobs duráveis ainda são backlog.
3. Contratos
3.1 Implementado — ServerBgRemovalProvider
src/lib/media/images/bg-removal/types.ts:
export interface ServerBgRemovalProvider {
id: string;
isConfigured(): boolean;
removeBackground(sourceUrl: string, opts?: { signal?: AbortSignal }): Promise<ArrayBuffer>;
}
- Provider fal:
createFalProvider(key, model?)(providers/fal.ts). - Registry:
pickServerBgProvider()retorna o 1ºisConfigured()na ordem deBG_REMOVAL_PROVIDERS(CSV, defaultfal);setBgRemovalRuntimeEnv(env)injeta os secrets do runtime Cloudflare.
3.2 Implementado — capacidades e profiles genéricos
type MediaCapabilityId = 'image.remove-background' | 'image.generate' | (string & {});
interface EndpointProfile<TInput, TVendorResponse = unknown> {
id: string;
capability: MediaCapabilityId;
provider: string;
endpoint: string;
inputAdapter(input: TInput): Record<string, unknown>;
outputNormalizer(response: TVendorResponse): { url: string; urls?: string[]; raw?: unknown };
costTier?: 'economy' | 'standard' | 'premium';
}
runMediaCapability(capability, input, { key, profileId, endpointOverride, signal });
- Profiles por endpoint: cada endpoint Fal mantém seu schema num adapter; não existe um payload universal artificial.
- Catálogo único de IDs Fal:
src/lib/media/generation/catalog.tsregistra os profiles; o ID default do BiRefNet vive no seu profile. - Transporte testável: recebe chave, timeout e
AbortSignalpor argumento; não lê secrets no cliente nem depende de Astro.
ServerBgRemovalProvider permanece como contrato de compatibilidade do domínio
de imagens, mas sua implementação Fal delega a execução à camada genérica.
MediaJob durável continua proposto para a fase de fila/webhook.
4. MVP desta rodada (implementado e validado)
A camada server-side agnóstica é usada de verdade pelo provider de remoção de fundo:
-
src/lib/media/generation/: contratos, catálogo, runner Fal, profile BiRefNet e transporte síncrono com timeout/abort e erros normalizados. -
bg-removal/(registry +providers/fal.ts+types.ts) — agnóstico viaBG_REMOVAL_PROVIDERS. -
providers/fal.ts: adapter compatível comServerBgRemovalProvider; chamarunMediaCapability('image.remove-background', ...), baixa o PNG e retornaArrayBuffer. Modelo opcional viaFAL_BG_MODEL. -
persist-alpha.ts→user-assets/ai-images/<uid>/<uuid>.png+ updateai_generated_images(comreplacePathpara substituir in-place). -
POST /api/ai/image/remove-bg(auth obrigatório) →200 {url,path,via}/503|502 {..., fallback:'client'}. -
background-removal-flow.ts(client): server-first + fallback imgly. -
Preservação + undo: chat e Viewer criam um novo objeto processado, mantêm o original em
metadata.original_url/IndexedDB e oferecem “Desfazer remoção” sem nova inferência. Falhas continuam fail-open.
Prova E2E (2026-07-15): createFalProvider contra fal-ai/birefnet/v2
retornou PNG 1024×1024 com hasAlpha=true (~806k px transparentes) em ~2,1s.
O transporte/catalogo possui smoke determinístico com fetch mockado em
tooling/test-fal-media.mjs.
Validação UI: build e revisão do fluxo de undo estão verdes. O smoke de cliques no Chrome MCP ficou pendente até haver uma sessão autenticada, pois a galeria e a remoção de fundo são auth-gated.
5. Fases seguintes (backlog)
- Jobs assíncronos + webhook: endpoints fal de longa duração via fila
(
/api/ai/media/jobs+ webhook idempotente/assinado); status polling. - Custos e orçamentos numéricos: a policy qualitativa de custo e os tiers
já existem; falta sincronizar preço por chamada no catálogo, telemetria em
ai_request_logse limites financeiros por perfil. - Upscale + preflight de impressão: subir resolução/DPI para estampa; checar dimensões mínimas antes de mandar pra produção.
- Replace / outpaint: edição generativa de regiões / expansão de canvas.
- Vídeo / try-on / áudio: capacidades novas sob o mesmo
MediaProvider.
Cada fase = nova(s) capacidade(s) + entrada no catálogo + (se longa) o runner de jobs. Nenhuma exige reescrever o que existe.
6. Privacidade e segurança
- Chave server-only:
FAL_KEYvive em.dev.vars(dev) / Pages secret (prod), lida viasetBgRemovalRuntimeEnv(locals.runtime.env). Nunca vai ao cliente. O fallback client (imgly) roda 100% no navegador, sem chave. - Auth:
/remove-bgexigelocals.user(401 sem sessão).replacePathé validado contra o prefixoai-images/<user.id>/(403 fora dele). - URLs: o
image_urlmandado ao fal precisa ser acessível (pública/assinada). O resultado é re-hospedado no nosso Storage (persistAlphaPng) — não dependemos da URL transiente do fal. - Retenção: PNG alpha e original ficam em objetos separados sob
user-assets/ai-images/<uid>/enquanto o undo estiver disponível. A referência anterior fica emai_generated_images.metadata.original_url. - Webhook (fase 2): quando entrar job async, o webhook deve ser idempotente + assinado (mesmo padrão do webhook C6/MP Pix).
7. Critérios de aceite, riscos, rollback
Aceite (MVP atual): ✅ todos atendidos.
- Provider agnóstico selecionável por env; ✅
/remove-bgretorna PNG alpha persistido para usuário logado; ✅- Fail-open: falha de provider mantém original e cai pro client; ✅
- Original preservado e undo persistido em
metadata.original_url; ✅ - E2E real com alpha comprovado. ✅
Riscos.
- Saldo fal esgotado →
502 fallback:client→ imgly 40MB no navegador (frágil em rede hostil). Mitigação futura: provider rembg no homelab (custo zero). - URL de origem não pública → fal não acessa. Mitigar validando a origem.
- Modelo/endpoint fal muda → isolado em
providers/fal.ts+FAL_BG_MODEL.
Rollback. BG_REMOVAL_PROVIDERS='' (ou remover FAL_KEY) desliga o server
path → tudo cai no fallback client, sem quebrar a UI. Reverter a rodada = git
revert dos arquivos da seção 8 (nada é destrutivo; sem migração de schema).
8. Implementado nesta rodada vs. backlog
| Item | Estado | Onde |
|---|---|---|
| Registry agnóstico server-side | ✅ implementado | src/lib/media/images/bg-removal/index.ts |
| Provider fal BiRefNet v2 | ✅ implementado | src/lib/media/images/bg-removal/providers/fal.ts |
Contrato ServerBgRemovalProvider | ✅ implementado | src/lib/media/images/bg-removal/types.ts |
| Persistência PNG alpha (Supabase) | ✅ implementado | src/lib/media/images/persist-alpha.ts |
Endpoint /api/ai/image/remove-bg | ✅ implementado | src/pages/api/ai/image/remove-bg.ts |
| Orquestração client + fallback imgly | ✅ implementado | src/lib/ai/background-removal-flow.ts, src/lib/media/images/background-removal.ts |
| Auto-bg no chat (P2, fail-open) | ✅ implementado | src/components/ai/ChatInterface.astro |
| E2E real fal (PNG com alpha) | ✅ provado | validação 2026-07-15 |
| Undo na UI (reverter remoção) | ✅ implementado | ImageViewer, ChatInterface, storage.ts |
MediaCapability + resultado/erro genéricos | ✅ implementado | src/lib/media/generation/types.ts |
| Runner por provider + profiles | ✅ MVP síncrono | src/lib/media/generation/index.ts, providers/fal/ |
| Catálogo único de IDs Fal | ✅ implementado | src/lib/media/generation/catalog.ts + profile Fal |
| Policy econômica Gemini → Fal especialista | ✅ implementado | image-magic.ts, /api/ai/image.ts |
| Perfis Fal geração (rápido, ilustração, tipografia, vetor, premium) | ✅ implementado | src/lib/media/generation/providers/fal/profiles.ts |
| Comparação 2× Gemini + 1× Fal especialista | ✅ implementado | image-prompt.ts, /api/ai/image.ts, ChatInterface.astro |
| Escolha e proveniência por variante | ✅ implementado | storage.ts, ImageGallery.astro |
| Endpoint autenticado de escolha sem clobber de metadata | ✅ implementado | /api/ai/image/select |
| Fallback Fal → Gemini | ✅ implementado | src/pages/api/ai/image.ts |
| Jobs async + webhook assinado | 🔲 backlog | §5 |
| Preço numérico/orçamento por perfil | 🔲 backlog | §5 |
| Upscale / preflight impressão | 🔲 backlog | §5 |
| Replace / outpaint / vídeo / try-on / áudio | 🔲 backlog | §5 |
9. Mapa de arquivos
src/lib/media/images/
├── bg-removal/
│ ├── index.ts # registry: pickServerBgProvider / setBgRemovalRuntimeEnv
│ ├── types.ts # ServerBgRemovalProvider
│ └── providers/fal.ts # createFalProvider (BiRefNet v2)
├── background-removal.ts # fallback client (imgly, ~40MB, import dinâmico)
├── persist-alpha.ts # persistAlphaPng → Supabase Storage
└── SPEC.md # spec detalhada desta pasta
src/lib/ai/
└── background-removal-flow.ts # orquestração client (server → fallback)
src/lib/media/generation/
├── types.ts # capability, profile, result e erros
├── catalog.ts # registro central capability → profile
├── index.ts # runMediaCapability + runners
└── providers/fal/
├── index.ts # runner Fal
├── profiles.ts # adapters e endpoint default
└── transport.ts # HTTP, timeout/abort e erros
src/pages/api/ai/image/
├── remove-bg.ts # server-side (fal) — NOVO
└── process.ts # persist do PNG imgly (client)
src/pages/api/ai/image.ts # geração comparativa: 2× Gemini + 1× Fal especialista
src/components/ai/
├── ChatInterface.astro # P2 auto-bg (bgRemoval: recommended)
└── ImageViewer.astro # botão "Remover fundo" + preview de cor de fundo
Envs: FAL_KEY (secret), FAL_BG_MODEL (default fal-ai/birefnet/v2),
BG_REMOVAL_PROVIDERS (CSV, default fal).
Roteamento de geração
O domínio usa os profiles google-free, fal-illustration, fal-typography,
fal-vector e fal-premium. Cada pedido tenta gerar duas amostras Gemini e uma
amostra Fal. A terceira rota é selecionada no servidor: Recraft ilustração para
pedidos comuns, Ideogram para texto proeminente, Recraft vetorial para vetor ou
logo e FLUX.2 Pro apenas para pedido premium explícito.
A comparação inteira consome uma solicitação da quota do produto. Cada variante
tem telemetria e proveniência próprias; comparison_group_id une as três e
selected_variant registra a escolha. Falhas parciais não removem resultados
válidos, e uma Fal não autorizada nunca é executada. Os detalhes do Magic Prompt
e da policy econômica estão em docs/IMAGE_MAGIC_PROMPT.md.
10. Referências
- fal.ai — modelo BiRefNet v2:
https://fal.ai/models/fal-ai/birefnet/v2 - fal.ai — HTTP/queue API:
https://docs.fal.ai/ - SPEC local:
src/lib/media/images/SPEC.md