Lição 13 · Curso de Fusão / Parte 2 · Mergulhos profundos · subsistema 7 de 7
Mergulho profundo · ports de mídia · CLONE (caminho cloud)

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.

Cada seção tem uma camada Simples e uma Técnico (com o código real). Comece pelo Simples.
O que você vai dominar
  • 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/error somem do payload e viram o wrapper Result — e o que isso protege.
  • Como os backends fetch mapeiam payloads de forma defensiva, com field fallbacks, sem abrir socket nos testes.
  • Por que o caminho de ML local faster-whisper foi deliberadamente marcado IGNORE na matriz.
Suposições tolas (assumimos pouco de você)
  • 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), de Zod (valida dados) e de fetch (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).
A ideia em uma frase Uma capacidade de mídia nova não é um projeto novo. É o mesmo molde repetido: valida a entrada, chama o port injetado, valida a saída, devolve Result. Trocam-se os schemas; a estrutura é a mesma. molde (kernel) áudio → text imagem → analysis mesma forma,schemas diferentes
01

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.

Por que isso importa: quando o padrão já está firmado, criar uma ferramenta de mídia nova é mecânico. Você não inventa arquitetura — você preenche o molde. Disciplina que escala é disciplina que economiza tempo e bug.
Infográfico: duas ferramentas (transcribe e analyzeImage) entram no MESMO trilho de quatro estágios — validar request (Zod, cross-field), backend injetado (fetch em produção / fake em teste), validar result (Zod, não confiável), e o Result final (ok | err). Um selo destaca que a única diferença são os schemas; o rodapé resume: uma nova ferramenta de mídia é um port + um kernel + um seam fetch fino.

As duas ferramentas compartilham um único trilho de quatro estágios. Só os schemas mudam de uma para a outra.

transcribe(req) / analyzeImage(req) — o mesmo trilho validar request Zod · cross-field backend injetado fetch (prod) / fake (test) validar result Zod · não confiável Result sucesso/erro vivem no wrapper Result, nunca inline — uma falha nunca se disfarça de texto/análise vazia os provedores cloud de STT + o caminho local faster-whisper colapsam em UM port injetado (ML local = IGNORE) analyzeImage é byte-a-byte a mesma forma; só os schemas mudam
Fig. 1 — O trilho de quatro estágios. As duas ferramentas o percorrem; só o gabarito (schema) de cada estágio muda.

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:

packages/hermes/src/media/types.ts:133–148
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.

02

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:

  1. Confere o pedido. Se está errado, devolve err — e nem chama o backend.
  2. Chama o backend injetado.
  3. Se o backend falhou, repassa o err dele tal e qual.
  4. Confere a resposta do backend (que é não confiável). Se está malformada, devolve err. Senão, ok.
early return — cada degrau é uma porta de saída err 1 request inválido→ err 3 backend !ok→ err 4 result malformado→ err tudo passou → ok(data)
Faça sua aposta antes de revelar

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?

Devolve 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(req, deps) — para no primeiro err 1 · safeParse(request) inválido → err 2 · await deps.backend port injetado 3 · !backend.ok ? repassa o err do backend 4 · safeParse(result) não confiável → err ok(parsed.data) ok três saídas err, uma saída ok nenhuma exceção atravessa o boundary
Fig. 2 — O kernel de 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 — transcribe
const 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.

Recap relâmpago 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.
03

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.

(audioUrl===undefined) !== (audioBase64===undefined) audioUrl audioBase64 resultado ausente ausente err ✗ presente ausente ok ✓ presente presente err ✗
Fig. 3 — A tabela-verdade do XOR. Só a linha do meio (exatamente uma fonte) passa; nenhuma e ambas falham fechado.
Por que !== 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". A=ausente? B=ausente? XOR um só presente → ok iguais → err

O schema com o refine

packages/hermes/src/media/types.ts:62–74 — transcriptionRequestSchema
z.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.

04

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.

A vitória: uma falha não consegue se disfarçar de text vazio. Silêncio real é ok({text:''}) — um sucesso legítimo. Uma falha é err — estruturalmente diferente. Os dois nunca se confundem.
de: dict plano do Python → para: Result + núcleo enxuto { ... } plano (Python) success error transcript provider colapsa Result<…> (wrapper ok | err) absorve success + error uma falha vira err, nunca ok vazio TranscriptionResult (núcleo) text (era transcript) provider? (opcional)
Fig. 4 — O colapso do envelope. success/error sobem para o wrapper Result; o núcleo guarda só text (+ provider?).
Flashcard · vire
O que é ok({text:''})?
clique para virar
Silêncio legítimo. Uma transcrição vazia é um sucesso real — o áudio simplesmente não tinha fala. Chega como ok, nunca confundido com falha.
Flashcard · vire
err
E uma falha de provedor?
clique para virar
É err. Estruturalmente distinta de ok({text:''}). Manter success/error fora do payload e dentro do Result é o que torna os dois inconfundíveis.
Flashcard · vire
transcripttext
Para onde foi o transcript do Python?
clique para virar
Virou o campo idiomático text em TranscriptionResult. O analysis da visão continuou analysis.

Os dois schemas de resultado

packages/hermes/src/media/types.ts:85–121
export 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.

05

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.

1 montar corpo 2 POST (fetch global) 3 mapear defensivo

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 — três falhas de transporte viram err network throw → err status não-2xx → err JSON inparseável → err mapTranscriptionRow — field fallback readField('text') ?? readField('transcript') asString(...) → text não-string vira '' — o Zod do kernel decide depois
Fig. 5 — Esquerda: 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 defensivo
const 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.

Defesa em profundidade Há um teste (linhas 248–262) que prova: um 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.
06

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, ElevenLabse 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.

Infográfico em duas zonas: na zona de cima (verde), cinco cartões cloud — Groq, OpenAI, Mistral, xAI, ElevenLabs — convergem por setas para uma única caixa TranscriptionBackend (1 port injetado), porque todos só fazem POST de áudio para um endpoint; na zona de baixo (esmaecida), o cartão faster-whisper (Python-ML, download de modelo + GPU) é cortado por um carimbo vermelho IGNORE — fora de escopo do kernel TS portável.

Os provedores cloud colapsam em um port; o caminho ML local faster-whisper é IGNORE deliberado, não esquecimento.

CaminhoO que éVeredicto
STT cloud (Groq/OpenAI/…)POST de áudio para um endpointCLONE → 1 port
faster-whisper localML em Python, GPU, download de modeloIGNORE
visão (1 modelo multimodal)uma chamada de LLM auxiliar de visãoCLONE → 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.

A matriz é uma decisão, não um acidente
CLONE ADAPT MERGE IGNORE ←

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.

07

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.

cinco cenários × em que estágio o trilho decide estágio 1 estágio 3 estágio 4 saída áudio válidook duas fonteserr nenhuma fonteerr backend caierr sem texterr
Fig. 6 — Onde cada cenário do demo abaixo é decidido. Note como "duas/nenhuma fonte" param já no estágio 1, antes do backend.
request

áudio válido (URL)

{ audioUrl: 'https://a.test/clip.ogg' }

estágio 1 · safeParse(request) → passa
Result

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) → ok

Repare: nos cenários "duas fontes" e "nenhuma fonte", o estágio 1 já barra — o backend nem é chamado.

Exemplo trabalhado · rastreando um request de visão malformado de ponta a ponta
1
Entra o request. analyzeImage({ imageUrl: 'https://i.test/x.png', prompt: 'o que é isto?' }, { backend }). O visionRequestSchema exige URL válida + prompt não-vazio. Passa.
2
Chama o backend injetado. Suponha que o backend (uma fake no teste) devolva ok({}) — um objeto sem o campo analysis.
3
O backend "deu ok". analyzed.ok é true, então o kernel não repassa erro aqui. Mas a resposta é não confiável — falta validar.
4
Re-valida com Zod. visionResultSchema.safeParse({}) falha (analysis é obrigatório). O kernel devolve err("Invalid vision result: …"). Falha fechada — nada vaza.
Agora você: mude o passo 2 para o backend devolver 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.)
08

Confusões comuns

"Isto fia um provedor de transcrição de verdade." Não — é a ESTRUTURA portada para ports-and-injection. O kernel não importa SDK; createFetchTranscriptionBackend/createFetchVisionBackend são seams finos de JSON genérico sobre um fetch injetável, então os testes nunca abrem um socket.
"Transcrição vazia significa que falhou." Não — um 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.
"O faster-whisper foi esquecido." Não — foi IGNORE deliberado na matriz: ML-Python (download de modelo, GPU) não cabe num kernel TS portável. Os provedores cloud, que são só "POST para um endpoint", viram um port injetado.
Cuidado Não confunda "o backend deu ok" com "a resposta é válida". Um backend pode devolver 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. backend disse ok estágio 4: Zod pode dar err
09

Recapitulação em slides

Seis cartões que fecham a lição. Use as setas, os pontos, ou as teclas .

1 · A forma

Uma forma, dois backends

transcribe e analyzeImage têm byte-a-byte a mesma forma: validar request → backend injetado → validar result → Result. Só os schemas mudam.

i

2 · O kernel

Quatro passos, para no primeiro erro

Valida request (senão err, sem chamar backend) → chama backend → se falhou, repassa → re-valida result → ok. Três saídas err, uma ok. Nunca lança.

ii

3 · O XOR

Exatamente uma fonte de áudio

Um .refine() com (a===undefined) !== (b===undefined) — um XOR. Nenhuma fonte falha; ambas falham; só uma passa. No boundary, antes do backend.

iii

4 · O envelope

success/error viram o Result

O dict plano do Python colapsa: success/error no wrapper, transcripttext. Silêncio é ok({text:''}); falha é err — nunca se confundem.

iv

5 · Os fetch backends

Defensivo + field fallback

Sem dep nova. postJson levanta throw/não-2xx/JSON ruim para err. O mapper cai de text para transcript; não-string vira '' e o Zod do kernel decide.

v

6 · A matriz

Cloud colapsa, local é IGNORE

Os STT cloud (Groq/OpenAI/…) viram um port. O faster-whisper local é IGNORE deliberado — ML-Python fora de escopo. Clonar o portável, ignorar o que não traduz.

vi
1 / 6 use
Você já viu o padrão sete vezes. Memória, aprendizado, curadoria, clarify, web, skills, mídia — todo subsistema @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.
10

Revisão — fixe o que importa

Quiz de revisão (3 perguntas)

Responda cada uma; o placar corre embaixo.

1. Um request de transcrição manda as duas coisas — audioUrl e audioBase64. O que acontece?
Correto: c. O .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.
2. Por que success/error não aparecem como campos de TranscriptionResult?
Correto: b. O 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.
3. Por que o caminho local faster-whisper da fonte foi marcado IGNORE na fusão?
Correto: d. As disposições CLONE/ADAPT/MERGE/IGNORE da matriz 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.
Acertos: 0/3
As cinco coisas para levar desta lição
  1. Uma capacidade de mídia nova é um port + um kernel + um seam fetch fino — não um projeto novo.
  2. O kernel valida nas duas pontas: o request (entrada) e o result do backend (saída não confiável).
  3. "Exatamente um" vira um XOR em .refine() que falha fechado antes do backend.
  4. success/error moram no Result, não no payload — silêncio (ok({text:''})) nunca vira falha.
  5. Cloud colapsa em um port; ML local é IGNORE deliberado — clone o portável e diga o que ignorou.
A melhor coisa para ler agora

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.