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-background já está executada pela camada genérica src/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. Ver src/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 AIProvider de 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; em 503/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 em ai_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 de BG_REMOVAL_PROVIDERS (CSV, default fal); 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.ts registra os profiles; o ID default do BiRefNet vive no seu profile.
  • Transporte testável: recebe chave, timeout e AbortSignal por 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 via BG_REMOVAL_PROVIDERS.

  • providers/fal.ts: adapter compatível com ServerBgRemovalProvider; chama runMediaCapability('image.remove-background', ...), baixa o PNG e retorna ArrayBuffer. Modelo opcional via FAL_BG_MODEL.

  • persist-alpha.tsuser-assets/ai-images/<uid>/<uuid>.png + update ai_generated_images (com replacePath para 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)

  1. Jobs assíncronos + webhook: endpoints fal de longa duração via fila (/api/ai/media/jobs + webhook idempotente/assinado); status polling.
  2. 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_logs e limites financeiros por perfil.
  3. Upscale + preflight de impressão: subir resolução/DPI para estampa; checar dimensões mínimas antes de mandar pra produção.
  4. Replace / outpaint: edição generativa de regiões / expansão de canvas.
  5. 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_KEY vive em .dev.vars (dev) / Pages secret (prod), lida via setBgRemovalRuntimeEnv(locals.runtime.env). Nunca vai ao cliente. O fallback client (imgly) roda 100% no navegador, sem chave.
  • Auth: /remove-bg exige locals.user (401 sem sessão). replacePath é validado contra o prefixo ai-images/<user.id>/ (403 fora dele).
  • URLs: o image_url mandado 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 em ai_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-bg retorna 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 esgotado502 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

ItemEstadoOnde
Registry agnóstico server-side✅ implementadosrc/lib/media/images/bg-removal/index.ts
Provider fal BiRefNet v2✅ implementadosrc/lib/media/images/bg-removal/providers/fal.ts
Contrato ServerBgRemovalProvider✅ implementadosrc/lib/media/images/bg-removal/types.ts
Persistência PNG alpha (Supabase)✅ implementadosrc/lib/media/images/persist-alpha.ts
Endpoint /api/ai/image/remove-bg✅ implementadosrc/pages/api/ai/image/remove-bg.ts
Orquestração client + fallback imgly✅ implementadosrc/lib/ai/background-removal-flow.ts, src/lib/media/images/background-removal.ts
Auto-bg no chat (P2, fail-open)✅ implementadosrc/components/ai/ChatInterface.astro
E2E real fal (PNG com alpha)✅ provadovalidação 2026-07-15
Undo na UI (reverter remoção)✅ implementadoImageViewer, ChatInterface, storage.ts
MediaCapability + resultado/erro genéricos✅ implementadosrc/lib/media/generation/types.ts
Runner por provider + profiles✅ MVP síncronosrc/lib/media/generation/index.ts, providers/fal/
Catálogo único de IDs Fal✅ implementadosrc/lib/media/generation/catalog.ts + profile Fal
Policy econômica Gemini → Fal especialista✅ implementadoimage-magic.ts, /api/ai/image.ts
Perfis Fal geração (rápido, ilustração, tipografia, vetor, premium)✅ implementadosrc/lib/media/generation/providers/fal/profiles.ts
Comparação 2× Gemini + 1× Fal especialista✅ implementadoimage-prompt.ts, /api/ai/image.ts, ChatInterface.astro
Escolha e proveniência por variante✅ implementadostorage.ts, ImageGallery.astro
Endpoint autenticado de escolha sem clobber de metadata✅ implementado/api/ai/image/select
Fallback Fal → Gemini✅ implementadosrc/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