OCR transcreve os glifos visíveis; vision entende a cena. Um dos 27 packages do monorepo dá ao motor um olho multimodal: describeImages valida, despacha em LOTES com paralelismo limitado e re-valida a saída não-confiável de um backend INJETADO — offline determinístico por default, MLX-VLM local (Qwen3-VL) sob --online. Em cima dele: o índice vision-index que NUNCA escreve no corpus, e o seam de forbidden-visuals do gate de vídeo do marketing.
O cabeçalho de packages/vision/src/types.ts:2–8 traça a fronteira: "Unlike @alembic/ocr (which transcribes the text VISIBLE in an image), a vision request maps an image to a natural-language DESCRIPTION produced by a local multimodal LLM (VLM)" — diagramas, screenshots de UI, gráficos, arquitetura e design, não só glifos. É "uma LLM multimodal local que LÊ a imagem" (a diretriz do founder, citada no mesmo cabeçalho), no estilo ports-and-injection da casa: o módulo importa zero Python, zero modelo, zero SDK.
O contrato é mínimo e Zod-fechado: um VisionRequest é { imagePath, prompt?, maxTokens? } (types.ts:30–37); um VisionResult é { imagePath, description, model? } — e a description PODE ser vazia, porque uma imagem em branco é um parse válido (types.ts:48–55). O imagePath ecoado no resultado torna qualquer lote re-juntável à origem.
(reqs[]) ⇒ Result<results[]> (types.ts:71–73). Carregar um VLM é caro; UMA carga de modelo amortiza N descrições, então a unidade de trabalho é o lote, não a imagem. O paralelismo entre lotes (as "várias threads" do founder) vive no kernel.Pense como… a diferença entre um datilógrafo e um crítico de arte diante do mesmo quadro: o datilógrafo (OCR) copia a plaquinha da moldura; o crítico (vision) descreve a cena, o estilo e a intenção. Onde a analogia quebra: este crítico roda a temperatura 0 — a MESMA imagem produz SEMPRE a mesma crítica, de propósito, para o motor poder cachear.
describeImages (packages/vision/src/vision.ts:74–103) é despacho puro: valida CADA request na borda (vision.ts:80–82 — um request inválido derruba a chamada inteira), fatia em chunks de DEFAULT_CHUNK_SIZE = 8 imagens (vision.ts:24), roda no máximo DEFAULT_CONCURRENCY = 2 lotes em voo (vision.ts:26 — a máquina continua responsiva), e então trata a resposta do backend como NÃO-CONFIÁVEL: cada resultado é re-validado com visionResultSchema.safeParse (vision.ts:97–99). Um err de qualquer lote colapsa a chamada toda (vision.ts:95) — nunca um sucesso parcial fingindo ser completo.
const chunks = chunk(validated, deps.chunkSize ?? DEFAULT_CHUNK_SIZE); const chunkOutcomes = await mapWithConcurrency( chunks, deps.concurrency ?? DEFAULT_CONCURRENCY, (batch) => deps.backend(batch), ); for (const outcome of chunkOutcomes) { if (!outcome.ok) return outcome; // fail-closed: 1 lote ruim = tudo err for (const result of outcome.value) { const parsed = visionResultSchema.safeParse(result); // saída NÃO-confiável if (!parsed.success) return err(new Error(`invalid vision result: …`)); out.push(parsed.data); } }
Offline (o default, $0): createOfflineVisionBackend devolve VLM(${req.imagePath}) — uma descrição canned derivada só do path (offline-backend.ts:39). O próprio arquivo avisa: "The text is NOT real recognition — it is a reproducible placeholder" (offline-backend.ts:9–10). Mesmo input ⇒ mesmo output; imagens distintas ⇒ descrições distintas; zero Python, zero rede, zero Date.now(). É o que mantém CI e runs herméticos honestos.
MLX (--online): createMlxVisionBackend é um wrapper de subprocess sobre o python/vision_describe.py empacotado no próprio pacote (mlx-backend.ts:62–63), com modelo default mlx-community/Qwen3-VL-30B-A3B-Instruct-4bit — o comentário no código registra o porquê: "MoE, 3B active — benchmarked fastest + most accurate on M5 Max" (mlx-backend.ts:47, constante em :56). O helper roda o VLM a temperatura 0, então input idêntico ⇒ descrição idêntica (mlx-backend.ts:10–11) — determinismo que permite cachear descrições em disco.
| Aspecto | offline-backend.ts | mlx-backend.ts |
|---|---|---|
| Custo | $0, in-process | $0 também (modelo LOCAL), mas pesado |
| Descrição | VLM(path) — placeholder honesto (:39) | leitura real da imagem, temp 0 (:10–11) |
| Lote | map puro | 1 subprocess por (prompt, maxTokens) distintos (:92–107) |
| Falha | nunca (sempre ok) | exit ≠ 0 ⇒ err com stderr (:169–172) |
| Imagem sem saída | — | description: '', nunca throw (:178–181) |
groupRequests (mlx-backend.ts:92–107) agrupa por par (prompt, maxTokens) — um lote uniforme (o caso comum) vira EXATAMENTE uma chamada de subprocess; um lote misto fan-out em uma chamada por par distinto, com merge de volta por path. Insertion-ordered ⇒ determinístico. O parse do stdout é linha-a-linha JSONL, pulando lixo e linhas _meta (mlx-backend.ts:110–128).alembic vision-index <family-root> (apps/cli/src/commands.ts:918–1025) constrói o índice de entendimento visual de uma família wiki: enumera os packages, colhe as imagens de cada media_manifest.json (commands.ts:894–900), descreve SÓ as ainda-não-indexadas e appenda { sourcePath, image, description, model } em <data-dir>/vision-index/<family>.jsonl (commands.ts:941). O doc do comando é explícito: "append-only, dedupe-by-image — a re-run appends 0. NEVER writes into the corpus" (commands.ts:922–925) — a única escrita do comando é no indexPath (commands.ts:991–999); o corpus é tratado como só-leitura.
runVisionIndex persiste SÓ descrições não-vazias (commands.ts:977–990): pular a vazia mantém a falha re-tentável.vision-index faz com essa imagem?commands.ts:981–982) exatamente para a falha continuar re-tentável — gravar a vazia a "carimbaria" como indexada. E vazio ≠ err: o schema aceita description: '' (types.ts:52), porque uma imagem em branco é um parse válido.O gate de QA de vídeo do marketing (packages/marketing-factory/src/validate.ts) precisa de visão para varrer frames por visuais proibidos — mas o pacote é dependency-light DE PROPÓSITO. A solução é um port: FrameDescriber (validate.ts:50–52), cujo default é o honesto noopFrameDescriber que devolve '' (validate.ts:179–181) — sem injeção, NENHUM forbidden-visual é detectável, e o gate não finge que olhou.
Os dentes reais moram na CLI, que já depende de @alembic/vision: createFrameDescriber (apps/cli/src/marketing-seams.ts:47–56) embrulha describeImages num describer de frame único. Dentro do gate, scanFrames descreve cada frame amostrado e testa desc.includes(normalize(forbidden)) — cada hit vira um VisualFlag (validate.ts:462–470), e flags viram falha: visual: N forbidden-visual flag(s) (validate.ts:583). A varredura só roda quando alguém a pediu: checkFrames = noBakedText || forbiddenVisuals.length > 0 (validate.ts:518) — o gate serve QUALQUER tipo de vídeo.
validateVideo usa como FrameDescriber para a varredura de forbidden-visuals?validate.ts:179–181) — o pacote não depende de @alembic/vision; é a CLI que injeta o describer real via createFrameDescriber (marketing-seams.ts:47–56). Um default "esperto" fingiria cobertura que não existe.Tudo $0 e read-only — verifique cada afirmação desta lição:
# o contrato e a fronteira vision ≠ ocr sed -n '1,25p' packages/vision/src/types.ts # o kernel: bordas Zod, chunk de 8, concorrência 2, fail-closed sed -n '24,26p;74,103p' packages/vision/src/vision.ts # o modelo default do backend MLX + temperatura 0 grep -n 'DEFAULT_MODEL\|temperature 0' packages/vision/src/mlx-backend.ts # o índice: dedupe, nunca-no-corpus, vazias puladas sed -n '918,1025p' apps/cli/src/commands.ts # rodar o índice offline ($0, determinístico) e ler o resultado alembic vision-index ~/Documents/Resources/Bookmarks --json head -3 ~/.alembic/vision-index/bookmarks.jsonl # o seam de forbidden-visuals (port + wiring real na CLI) sed -n '50,52p;174,181p' packages/marketing-factory/src/validate.ts sed -n '43,56p' apps/cli/src/marketing-seams.ts
model em cada linha?" — pense em invalidação de cache quando o modelo default mudar. Próxima lição: o irmão transcritor — @alembic/ocr, o mesmo desenho de ports com um contrato de wire copiado verbatim do infer.py.