Mídia profunda — transcribe / analyzeImage
Duas ferramentas — fala-para-texto e entendimento de imagem — construídas sobre exatamente o mesmo padrão de ports que você já viu seis vezes. Cada uma é um pequeno kernel de dispatch: valida o request (Zod), chama o backend injetado, re-valida o resultado não confiável (Zod) e devolve um Result. Os backends de produção são fetch sem nenhuma dependência. Esta é a prova de fechamento de que a disciplina escala: uma capacidade nova é um port + um kernel + um seam fetch fino — nada mais. Um CLONE do transcription_tools.py (1799 LOC) + vision_tools.py do Hermes, só o caminho CLOUD.
- Por que duas ferramentas tão diferentes têm byte-a-byte a mesma forma (e por que isso é a lição).
- Como a regra "exatamente uma fonte de áudio" vira um XOR em Zod que falha fechado antes do backend.
- Por que
success/errorsomem do payload e viram o wrapper Result — e o que isso protege. - Como os backends
fetchmapeiam payloads de forma defensiva, com field fallbacks, sem abrir socket nos testes. - Por que o caminho de ML local
faster-whisperfoi deliberadamente marcado IGNORE na matriz.
- Você sabe que uma função pode devolver um valor em vez de lançar um erro. Só isso.
- Você já ouviu falar de
Result(ok | err), deZod(valida dados) e defetch(faz uma chamada HTTP) — se não, as caixas Técnico recapitulam. - Você não precisa ter visto as Lições 5–12; mas se viu, vai reconhecer o mesmo padrão de novo (esse é o ponto).
Result. Trocam-se os schemas; a estrutura é a mesma.
Uma forma, dois backends
Imagine uma esteira de fábrica com quatro estações: a peça entra, é conferida, vai para a máquina, é conferida de novo, e sai embalada. Agora rode na mesma esteira duas peças bem diferentes — um áudio (para virar texto) e uma imagem (para virar uma descrição). A esteira não muda. Só muda o gabarito de conferência de cada peça.
É isso que transcribe (áudio → texto) e analyzeImage (imagem → análise) fazem. As duas funções têm a mesma forma: validar pedido → chamar backend injetado → validar resposta → devolver Result. A única diferença são os schemas de cada uma.
As duas ferramentas compartilham um único trilho de quatro estágios. Só os schemas mudam de uma para a outra.
Os dois ports — o módulo não importa SDK nenhum
Cada chamada de rede vira um port injetado: uma única função que devolve um Result. Veja os dois tipos lado a lado — o módulo de tipos só importa z (Zod) e Result:
export type TranscriptionBackend = ( req: TranscriptionRequest, ) => Promise<Result<TranscriptionResult, Error>>; export type VisionBackend = ( req: VisionRequest, ) => Promise<Result<VisionResult, Error>>;
Por que dois tipos idênticos na forma?
Porque a forma é a contratação: "me dê um request validado, eu te devolvo um Result, e eu nunca lanço". Um é produção (fetch sobre um endpoint), o outro é teste (uma fake que devolve um Result fixo). O kernel não sabe nem se importa qual dos dois recebeu — só vê o port.
O kernel de dispatch
O "kernel" é o miolo: as poucas linhas que fazem o dispatch. Ele faz quatro coisas em ordem e para no primeiro erro. Em português:
- Confere o pedido. Se está errado, devolve
err— e nem chama o backend. - Chama o backend injetado.
- Se o backend falhou, repassa o
errdele tal e qual. - Confere a resposta do backend (que é não confiável). Se está malformada, devolve
err. Senão,ok.
O backend devolveu um objeto com provider: 'groq' mas sem o campo text. O kernel já tinha aprovado o request. O que ele faz com essa resposta?
err. A resposta do backend é tratada como não confiável e re-validada por Zod no boundary de saída. Sem text (que é obrigatório), o safeParse falha e o kernel devolve err("Invalid transcription result: …") — nunca um ok meia-boca. (É exatamente o caso de teste das linhas 86–93.)transcribe: quatro passos, três saídas err possíveis, uma saída ok. Nunca lança.O kernel inteiro, verbatim
packages/hermes/src/media/media.ts:51–68 — transcribeconst parsedReq = transcriptionRequestSchema.safeParse(req); if (!parsedReq.success) { return err(new Error(`Invalid transcription request: ${parsedReq.error.message}`)); } const transcribed = await deps.backend(parsedReq.data); if (!transcribed.ok) return transcribed; // falha do backend ⇒ err const parsed = transcriptionResultSchema.safeParse(transcribed.value); if (!parsed.success) { return err(new Error(`Invalid transcription result: ${parsed.error.message}`)); // saída não confiável } return ok(parsed.data);
analyzeImage é a MESMA coisa
analyzeImage (linhas 76–93) é byte-a-byte a mesma forma — validar request → backend → validar result — diferindo só nos schemas (visionRequestSchema / visionResultSchema). Essa simetria é a lição: uma vez firmado o padrão, a próxima ferramenta de mídia é mecânica.
safeParse não lança: devolve {success:true,data} ou {success:false,error}. Por isso o kernel consegue ser todo "return err / return ok" sem um único try/catch.Validação cross-field: exatamente uma fonte
O request de transcrição modela uma fonte de áudio portável — ou uma URL que o backend baixa, ou bytes em base64 embutidos. A regra é: exatamente uma das duas. Não pode ser nenhuma. Não pode ser as duas. Esse tipo de regra que olha dois campos ao mesmo tempo chama-se validação cross-field.
Em Zod isso é um .refine() — uma checagem extra que roda depois das checagens de campo. Aqui ela é um XOR (ou-exclusivo): verdadeira só quando exatamente um dos dois está presente.
!== sobre dois === undefined? Cada === undefined é um booleano ("este campo está ausente?"). O !== entre dois booleanos é um XOR: verdadeiro só quando eles diferem — isto é, um presente e o outro ausente. É a forma mais enxuta de escrever "exatamente um".
O schema com o refine
packages/hermes/src/media/types.ts:62–74 — transcriptionRequestSchemaz.object({ audioUrl: z.string().url('audioUrl must be a valid URL').optional(), audioBase64: z.string().min(1, 'audioBase64 cannot be empty').optional(), mimeType: z.string().min(1).optional(), }).refine( (req) => (req.audioUrl === undefined) !== (req.audioBase64 === undefined), { message: 'exactly one of audioUrl or audioBase64 is required' }, );
Dois testes fixam os dois modos de falha
A suíte pina ambos os modos de falha — nenhuma fonte (linhas 50–60) e as duas fontes (62–70) — cada um rejeitado no boundary antes de o backend ser chamado. O teste de "nenhuma fonte" ainda usa uma flag called=false para provar que o backend não foi tocado. Um terceiro teste (72–77) rejeita uma audioUrl que não é URL.
O colapso do envelope
A fonte em Python devolve um dicionário plano: {success, transcript, provider, error} para STT e {success, analysis} para visão. Tudo misturado: o status do sucesso, o dado, e o erro, no mesmo saco.
No Alembic isso colapsa. O success/error some do payload e vira o wrapper Result (que já é "deu certo ou deu errado"). O núcleo de sucesso é enxugado: transcript vira o idiomático text (com um provider opcional de proveniência); analysis continua analysis.
text vazio. Silêncio real é ok({text:''}) — um sucesso legítimo. Uma falha é err — estruturalmente diferente. Os dois nunca se confundem.success/error sobem para o wrapper Result; o núcleo guarda só text (+ provider?).ok({text:''})?ok, nunca confundido com falha.err. Estruturalmente distinta de ok({text:''}). Manter success/error fora do payload e dentro do Result é o que torna os dois inconfundíveis.transcript do Python?text em TranscriptionResult. O analysis da visão continuou analysis.Os dois schemas de resultado
packages/hermes/src/media/types.ts:85–121export const transcriptionResultSchema = z.object({ text: z.string(), // era `transcript`; pode ser '' (silêncio) provider: z.string().min(1).optional(), // proveniência opcional }); export const visionResultSchema = z.object({ analysis: z.string(), // continua `analysis` });
Note: text é z.string() sem .min(1) — string vazia é válida de propósito (silêncio). Já o request de visão exige prompt não-vazio (.min(1)), porque pedir análise sem pergunta não faz sentido.
Os backends fetch — mapeamento defensivo, field fallbacks
Como o backend de web (Lição 11), os backends de mídia são fetch finos sobre o fetch global do Node — sem nenhuma dependência nova, sem SDK. Eles fazem três coisas: montam o corpo, fazem o POST, e mapeiam o payload de volta para o formato enxuto.
O mapeamento é defensivo: como o kernel re-valida tudo depois, o mapper pode ser tolerante. Se o provedor mandou text, ótimo; se mandou o campo legado transcript, ele cai nele (fallback). Se o campo veio com tipo errado, vira string vazia — e aí o Zod do kernel decide.
postJson levanta toda falha de transporte para err. Direita: o mapper faz fallback de text para transcript.O mapeamento defensivo de linha
packages/hermes/src/media/fetch-backends.ts:163–178 — mapeamento defensivoconst mapTranscriptionRow = (payload) => { const provider = readField(payload, 'provider'); return { text: asString(readField(payload, 'text') ?? readField(payload, 'transcript')), // fallback ...(typeof provider === 'string' && provider.length > 0 ? { provider } : {}), }; }; const mapVisionRow = (payload) => ({ analysis: asString(readField(payload, 'analysis') ?? readField(payload, 'content')), // fallback });
postJson — o muro fail-closed do transporte
O postJson (linhas 139–155) usa tryCatchAsync para envolver tanto a chamada quanto o response.json(), e checa response.ok no meio. As três falhas de transporte — throw de rede, status não-2xx, JSON inparseável — viram err cada uma. O fetch em si é um campo de config injetável (default: o global, linhas 90/117), então os testes injetam um fetch fake e nunca abrem um socket.
analysis: 12345 (número, não string) é coagido a '' pelo asString e passa como string válida — o mapper nunca vaza um não-string adiante. Mapper defensivo + Zod do kernel trabalham juntos.Por que o ML local é IGNORE — uma decisão deliberada da matriz
A fonte suporta vários provedores cloud de STT — Groq, OpenAI, Mistral, xAI, ElevenLabs — e um caminho local faster-whisper (ML em Python, território de download de modelo e GPU). A matriz de fusão marca o caminho local como IGNORE.
Por quê? Porque ele é amarrado a ML-Python e fora de escopo para um kernel TypeScript portável. Enquanto isso, todos os provedores cloud colapsam em um único port injetado (todos só fazem POST de áudio para um endpoint). Esta é a disciplina funcionando como projetada: clone a estrutura portável, ignore o que não traduz — e diga isso explicitamente.
Os provedores cloud colapsam em um port; o caminho ML local faster-whisper é IGNORE deliberado, não esquecimento.
| Caminho | O que é | Veredicto |
|---|---|---|
| STT cloud (Groq/OpenAI/…) | POST de áudio para um endpoint | CLONE → 1 port |
| faster-whisper local | ML em Python, GPU, download de modelo | IGNORE |
| visão (1 modelo multimodal) | uma chamada de LLM auxiliar de visão | CLONE → 1 port |
O comentário de types.ts (linhas 15–20) é explícito: o port TranscriptionBackend mapeia o dispatch de provedor STT da fonte — só o caminho CLOUD (Groq / OpenAI / Mistral / xAI / ElevenLabs, todos POST de áudio para um endpoint). O provedor local faster-whisper é IGNORE (ML em Python, fora de escopo). Os desvios deliberados listados (linhas 40–48) incluem o cascade de auto-detecção de seis provedores, o registry de comandos, o dispatch de plugins, o caminho faster-whisper, resize/dimension caps de base64, filtragem SSRF/website-policy, ciclo de vida de temp-file e o registro de tool-schema da OpenAI — tudo na camada de tool/security/transport ainda não fiada, fora de escopo para o kernel de dados + dispatch.
As disposições CLONE / ADAPT / MERGE / IGNORE são deliberadas. O caminho de ML local não traduz para um kernel TS sem dependências, então é explicitamente ignorado; o dispatch de STT cloud — que é só "POST de áudio para um endpoint" — vira um TranscriptionBackend. Os testes injetam um fetch fake provando o mapeamento e os caminhos fail-closed do transporte (não-2xx, throw de rede, JSON inparseável — cada um → err) sem abrir um socket. Um teste até prova que um campo de payload não-string falha fechado pelo gate Zod do kernel — defesa em profundidade, de novo.
Demo: o dispatch ao vivo
Escolha um cenário de request e veja o trilho decidir. Cada botão mostra o que o kernel faz em cada estágio — e qual Result sai no fim.
áudio válido (URL)
{ audioUrl: 'https://a.test/clip.ogg' }
ok({ text: 'hello world', provider: 'groq' })
O backend respondeu, o resultado foi re-validado por Zod e passou.
estágios 2–4 · backend → safeParse(result) → okRepare: nos cenários "duas fontes" e "nenhuma fonte", o estágio 1 já barra — o backend nem é chamado.
analyzeImage({ imageUrl: 'https://i.test/x.png', prompt: 'o que é isto?' }, { backend }). O visionRequestSchema exige URL válida + prompt não-vazio. Passa.ok({}) — um objeto sem o campo analysis.analyzed.ok é true, então o kernel não repassa erro aqui. Mas a resposta é não confiável — falta validar.visionResultSchema.safeParse({}) falha (analysis é obrigatório). O kernel devolve err("Invalid vision result: …"). Falha fechada — nada vaza.ok({ analysis: 42 }) (um número). O fetch-backend coagiria isso a '' via asString antes de chegar no kernel — então qual seria o Result? (Resposta: ok({ analysis: '' }) — string válida; é exatamente o teste das linhas 248–262.)Confusões comuns
createFetchTranscriptionBackend/createFetchVisionBackend são seams finos de JSON genérico sobre um fetch injetável, então os testes nunca abrem um socket.text vazio é legítimo (silêncio) e chega como ok({text:''}). Uma falha é err. Manter success/error fora do payload e dentro do Result é exatamente o que torna os dois inequívocos.ok com um payload malformado. Por isso existe o estágio 4: o kernel re-valida a saída mesmo quando o backend disse que deu certo.
Recapitulação em slides
Seis cartões que fecham a lição. Use as setas, os pontos, ou as teclas ← →.
@alembic/hermes entregue obedece à mesma disciplina: injete os ports, devolva Result, valide entrada não confiável com Zod, nunca lance, sem Date.now()/Math.random(). Isso é a Lição 5 tornada concreta, sete vezes. Releia a Lição 5 agora e ela deve soar como um resumo de tudo isto.Revisão — fixe o que importa
Responda cada uma; o placar corre embaixo.
audioUrl e audioBase64. O que acontece?.refine do schema é um XOR: (audioUrl===undefined) !== (audioBase64===undefined) só é verdadeiro com exatamente uma fonte. As duas (ou nenhuma) falham fechado com err — nunca lança, nunca chega no backend.success/error não aparecem como campos de TranscriptionResult?success/error do envelope plano do Python viram o ok/err do Result. A vantagem: uma transcrição vazia (silêncio real) é ok({text:''}), nunca confundida com falha, que é err.faster-whisper da fonte foi marcado IGNORE na fusão?TranscriptionBackend.- Uma capacidade de mídia nova é um port + um kernel + um seam fetch fino — não um projeto novo.
- O kernel valida nas duas pontas: o request (entrada) e o result do backend (saída não confiável).
- "Exatamente um" vira um XOR em
.refine()que falha fechado antes do backend. success/errormoram noResult, não no payload — silêncio (ok({text:''})) nunca vira falha.- Cloud colapsa em um port; ML local é IGNORE deliberado — clone o portável e diga o que ignorou.
Abra packages/hermes/src/media/media.ts e leia transcribe (51–68) e analyzeImage (76–93) lado a lado. São ~18 linhas cada e idênticas na forma — a prova mais curta de tudo o que esta lição diz.