Curso / Lição 13
Lição 13 · Mergulho profundo · subsistema 7 de 7

transcribe / analyzeImage — portas de mídia

Duas ferramentas — fala-em-texto e compreensão de imagem — construídas no exato mesmo padrão de portas que você já viu seis vezes. Cada uma é um kernel de despacho minúsculo: validar o pedido (Zod), chamar o backend injetado, re-validar o resultado não confiável (Zod), retornar um Result. Esta é a prova de encerramento de que a disciplina escala: uma capacidade nova é uma porta + um kernel + uma costura fetch fina — nada mais.

Leia primeiro (fonte de verdade — no repositório)
packages/hermes/src/media/ — media.ts · types.ts · fetch-backends.ts · media.test.ts

CLONE da estrutura do transcription_tools.py (1.798 LOC) + vision_tools.py do Hermes — só o caminho CLOUD. Por que importa pra missão: é o sétimo subsistema entregue do @alembic/hermes, e ele fecha a tese de que toda capacidade do Hermes vira o mesmo desenho. Fonte de proveniência: docs/hermes-complete-map.md §3.5 (transcrição) e §3.6 (visão).

Objetivos desta lição
  • Reconhecer a forma de 4 estágios do kernel (validar pedido → backend → validar resultado → Result) e por que transcribe e analyzeImage são idênticos nela.
  • Ler a refinação Zod "exatamente uma fonte de áudio" como um XOR e prever as quatro entradas.
  • Explicar o colapso do envelope: por que success/error viram o Result e não campos do payload.
  • Justificar a decisão da matriz de IGNORAR o caminho local faster-whisper e CLONAR só os cinco provedores cloud numa única porta.
0
portas · TranscriptionBackend + VisionBackend
0
casos de teste (3 describe)
0
SDKs importados · zero socket nos testes
0
provedores cloud → 1 porta · local IGNORADO

01 · Duas ferramentas, uma única forma

Imagine um balcão de atendimento: você entrega um pedido, um funcionário terceirizado (que você pode trocar) faz o trabalho lá fora, e o balcão confere o que voltou antes de te devolver. O cliente do balcão nunca fala direto com o terceirizado. As duas ferramentas de mídia são exatamente isso — e a "pessoa terceirizada" é uma porta injetada: uma única função que retorna um Result. O módulo não importa nenhum SDK.

Em uma frase: cada ferramenta é um balcão que (1) confere o seu pedido, (2) chama um trabalhador que dá pra trocar, (3) confere o que ele devolveu, e (4) te entrega num envelope que diz "deu certo" ou "deu erro". Trocar de provedor = trocar o trabalhador, sem mexer no balcão.
Tecnicamente: o ponto de extensão é um port (tipo de função) injetado via dependências. TranscriptionBackend e VisionBackend são ambos (req) => Promise<Result<…, Error>>. Em produção, uma impl baseada em fetch; em teste, um fake. O kernel depende do tipo, nunca de um backend concreto — ports-and-injection (Lição 05).
// 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>>;
ARQUITETURA · duas portas de mídia, dois kernels, uma forma (o balcão)
transcribe() áudio → texto analyzeImage() imagem → análise kernel de despacho valida → chama → valida não importa SDK kernel de despacho mesma forma, outro schema não importa SDK porta injetada TranscriptionBackend (req) => Promise<Result> VisionBackend (req) => Promise<Result> prod · fetch (sem dep) teste · fake (sem socket) prod · fetch (1 modelo) teste · fake (sem socket) O cliente do balcão nunca fala com o terceirizado: só com o kernel, que devolve um Result.

A simetria é a lição

As duas ferramentas não são "parecidas" — são a mesma forma, diferindo só nos schemas. Veja lado a lado: troque os nomes dos tipos e um kernel vira o outro.

transcribe vs analyzeImage · mesma forma, schemas diferentes (comparativo)
transcribe analyzeImage transcriptionRequestSchema (XOR) visionRequestSchema (url + prompt) await deps.backend(req) await deps.backend(req) transcriptionResultSchema {text, provider?} visionResultSchema {analysis} return ok(parsed.data) return ok(parsed.data) Linhas 1 e 3 mudam (os schemas); linhas 2 e 4 são literalmente idênticas. Padrão estabelecido = nova ferramenta mecânica.

Há, porém, uma assimetria que vale isolar — e ela está só no estágio 1. O pedido de transcribe é um XOR ("exatamente uma" de duas fontes); o de analyzeImage é o oposto, um AND: precisa de imageUrl (URL válida) e prompt (não-vazio), os dois obrigatórios. Mesma forma de kernel, portões de entrada de polaridades opostas.

DOIS PORTÕES DE ENTRADA · transcribe = XOR (escolha) · analyzeImage = AND (ambos) (comparativo)
transcribe · portão XOR audioUrl? audioBase64? XOR exatamente 1 passa · nenhuma/ambas → err analyzeImage · portão AND imageUrl (URL) prompt (não-vazio) AND os dois passa · faltar um → err Estágio 1 difere na polaridade (XOR vs AND); estágios 2–4 são idênticos. Provas: prompt vazio e URL inválida → "Invalid vision request" (media.test.ts:105–122).

02 · O kernel de transcribe, linha a linha

Aqui está o balcão inteiro, sem pular nada. São quatro movimentos e três pontos onde o caminho pode virar um err — e em nenhum deles a função lança uma exceção. Repare que a falha do backend é repassada como está (return transcribed), preservando o erro original.

// 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 (repassado)

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);
O KERNEL EM 4 ESTÁGIOS · três saídas err, nenhuma exceção lançada
1 · valida pedido Zod safeParse 2 · backend injetado await deps.backend(req) 3 · valida resultado Zod safeParse (não confiável) ok(parsed.data) { text, provider? } err "Invalid … request" backend nunca é chamado return transcribed err do backend, repassado err "Invalid … result" (saída malformada) Três caminhos para err · zero throw · o sucesso é o único caminho que produz ok.

analyzeImage (media.ts:76-93) é byte-a-byte a mesma forma — valida imageUrl + prompt, chama o backend, valida {analysis}, retorna Result. A simetria não é coincidência: é o padrão da Lição 05 aplicado mais uma vez.

03 · Validação cruzada: exatamente uma fonte de áudio

O pedido de transcrição modela uma fonte de áudio portável — uma URL que o backend busca, ou base64 inline — e impõe "exatamente uma" com uma refinação Zod. Pense num formulário com duas caixas: a regra não é "preencha pelo menos uma", é "preencha exatamente uma". Isso é um XOR (ou-exclusivo).

// 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' },
);

O truque é o !== sobre dois testes === undefined: comparar dois booleanos com "diferente" é precisamente um XOR — verdadeiro só quando exatamente um dos lados é verdadeiro. Veja as quatro entradas possíveis:

A TABELA-VERDADE DO XOR · só "exatamente uma fonte" passa (comparativo)
audioUrl audioBase64 XOR = veredito — (undefined) — (undefined) false ✗ err "https://…ogg" — (undefined) true ✓ aceita — (undefined) "AAAA" true ✓ aceita "https://…ogg" "AAAA" false ✗ err Dois testes ===undefined comparados com !== ⇒ verdadeiro só quando UM lado difere do outro. Dois testes (50–71) fixam "nenhuma" e "ambas".
Preveja antes de continuar
Um pedido de transcrição fornece tanto audioUrl quanto audioBase64. O que acontece — o backend escolhe um? Lança exceção? Chute antes de revelar.
Falha fechada com err, na fronteira, antes do backend. O XOR vê os dois lados presentes (linha 4 da tabela), a refinação devolve false, e o kernel retorna err("exactly one of audioUrl or audioBase64 is required"). Nada de "o backend decide": ele nunca é chamado — o teste em media.test.ts:62-70 prova exatamente isso. E nunca lança: é um Result, não uma exceção.
Experimente abaixo. O validador interativo logo adiante (seção 03, fim) deixa você ligar/desligar cada fonte e ver o XOR e o veredito em tempo real — é a tabela acima virando algo que você controla.
VALIDADOR INTERATIVO · ligue/desligue cada fonte e veja o XOR decidir
audioUrl presente audioBase64 ausente veredito ✓ aceita XOR( presente , ausente ) = true → exatamente uma fonte → passa para o backend. Tudo offline, sem rede — é a mesma refinação Zod de types.ts:71-74 calculada na sua frente.

04 · O colapso do envelope

A fonte Python retorna dicts planos: {success, transcript, provider, error} para STT e {success, analysis} para vision. O port não copia esses campos cegamente — ele colapsa o envelope. success/error viram o ok/err do Result; o núcleo de sucesso é aparado e renomeado para o idioma do motor.

DE DICT PLANO (PYTHON) A Result<T> (TYPESCRIPT) · o que vai pra onde
Python · dict plano success: true/false error: "…" | None transcript: "olá" provider: "groq" TypeScript · Result<T> o wrapper Result ok<T> | err<Error> text: "olá" (era transcript) provider?: "groq" success + error → o envelope Result · transcript → text · provider → provider (opcional) · vision: analysis → analysis Ganho: uma falha não pode se disfarçar de text vazio — é err, distinto de ok({text:''}).
O ganho concreto. Mantendo success/error fora do payload, um transcript vazio (silêncio real) chega como ok({text:''}) — legítimo — enquanto uma falha é err. São estruturalmente distintos. Num dict plano {success:true, transcript:''} versus {success:false, error:'…'} você teria de ler o campo success a cada chamada e torcer para ninguém esquecer; aqui o tipo te obriga a tratar os dois ramos.

SILÊNCIO ≠ FALHA · o discriminante que o colapso garante (comparativo)
silêncio (legítimo) ok({ text: '' }) o áudio não tinha fala — sucesso, com texto vazio falha err(new Error('…')) provedor caiu / saída malformada — erro carregado No dict plano os dois poderiam virar "transcript vazio". Aqui o tipo separa: ok vs err. Impossível confundir.

05 · Trace um pedido na mão (passo a passo → agora você)

Você viu o kernel e o XOR. Agora execute-os na cabeça, devagar, para um pedido concreto — depois um caso é seu. Recuperar o procedimento (não só ver o resultado) é o que fixa.

Exemplo resolvido · transcribe({ audioUrl: 'https://a.test/clip.ogg' }) com backend ok({text:'hello world', provider:'groq'})
1
Valida o pedido. transcriptionRequestSchema.safeParse: audioUrl presente, audioBase64 ausente → XOR = true. parsedReq.success é true; segue.
2
Chama o backend injetado. await deps.backend(parsedReq.data) devolve ok({text:'hello world', provider:'groq'}). Como transcribed.ok é true, não repassa erro.
3
Valida o resultado não confiável. transcriptionResultSchema.safeParse: text é string ✓, provider é string não-vazia ✓ → parsed.success é true.
4
Veredito. return ok(parsed.data)ok({text:'hello world', provider:'groq'}). É exatamente o que o teste media.test.ts:38-42 afirma com toEqual.
Agora você: transcribe({ provider:'groq' }) — sem audioUrl e sem audioBase64, e o backend (se chamado) devolveria ok({text:'x'}). Onde para e o que retorna? Trace antes de revelar.
Para no passo 1. O XOR vê nenhuma fonte (ambos undefined) → falseparsedReq.success é falsereturn err("Invalid transcription request: …"). O backend nunca é chamado (o teste media.test.ts:50-60 verifica called === false). Lição: a validação de pedido é uma porta fechada — saída malformada nem alcança o trabalhador terceirizado.

06 · Os backends fetch — mapeamento defensivo, fallbacks de campo

Como o backend web (Lição 11), os backends de mídia são impls fetch finas sobre o fetch global (Node 18+, sem node-fetch, sem SDK, sem nova dependência). O fetch é um campo de config injetável, então os testes usam um fake e nunca abrem socket. Como o kernel re-valida tudo, o mapeador pode ser tolerante:

// 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
});
Em uma frase: o mapeador lê o campo "óbvio" e, se ele faltar, tenta um nome alternativo (texttranscript, analysiscontent). Qualquer coisa estranha vira string vazia. Ele não precisa ser rígido porque o kernel confere de novo depois.
Tecnicamente: readField só lê de objetos planos (senão undefined); ?? faz o fallback de campo; asString coage não-strings a ''. O provider só entra no objeto se for string não-vazia (spread condicional). É defesa em profundidade: o mapeador tolera, o Zod do kernel garante.

O transporte em si — postJson — é o ponto onde falhas viram err. Três modos de falha, um destino:

postJson · FLUXO DE TRANSPORTE FALHA-FECHADO (139–155)
tryCatchAsync(fetch POST) o fetch lançou? SIM err (rede, ex. ECONNREFUSED) não response.ok (2xx)? NÃO err "HTTP 503 …" SIM tryCatchAsync(response.json()) parseou? → ok(payload) · senão err ok(payload) → mapeador → kernel Zod

Os três modos de falha têm testes dedicados: não-2xx → err (207–216), throw de rede → err (218–227), JSON não parseável → err (229–246). Nenhum abre socket; todos injetam um fetch fake.

FALLBACK DE CAMPO · o mapeador tenta o nome alternativo antes de desistir
transcribe: text ?? se ausente transcript asString não-string ⇒ '' analyzeImage: analysis ?? se ausente content asString não-string ⇒ '' Testes de fallback: transcript (173–181) e content (195–205). O kernel ainda re-valida — tolerância no mapeador, garantia no Zod.
Preveja antes de continuar
O backend de vision recebe um payload onde analysis é o número 12345 (não uma string). O kernel re-valida com Zod, que exige analysis: z.string(). O resultado final é err de schema? Chute antes de revelar.
Não — é ok({analysis:''}). Sutil: o mapeador (asString) coage o número a '' antes de o kernel ver o valor. Então o Zod recebe uma string válida (vazia) e aprova. O teste media.test.ts:248-262 afirma exatamente {analysis:''}. A lição é a defesa em profundidade: o não-string nunca alcança o portão Zod — o mapeador já o neutralizou. (O err de "Invalid … result" é reservado para faltar o campo inteiro, como nos testes 86–93 e 131–137.)

07 · Por que o ML local é IGNORADO — uma decisão deliberada da matriz

A fonte suporta seis caminhos de transcrição: cinco provedores cloud (Groq, OpenAI Whisper, Mistral Voxtral, xAI Grok STT, ElevenLabs Scribe) e um caminho local faster-whisper (ML Python, baixa ~150 MB de modelo, território de GPU). A matriz de fusão não trata os seis igual. Diante de cada capacidade do Hermes, ela escolhe uma disposição — e aqui o veredito do docs/hermes-complete-map.md é "ADAPT cloud, IGNORE/sidecar local".

A DECISÃO DA MATRIZ · uma capacidade do Hermes → CLONE / ADAPT / MERGE / IGNORE (fluxograma)
capacidade do Hermes (ex.: transcrever áudio) traduz p/ kernel TS sem dependências? NÃO (ML Python) IGNORE (ou sidecar) SIM o motor já tem equivalente? SIM MERGE NÃO CLONE / ADAPT (a estrutura) caminho da transcrição: cloud (POST áudio) → SIM → ADAPT numa porta faster-whisper → NÃO → IGNORE
A disciplina funcionando como pretendido

Os cinco provedores cloud são, no fundo, a mesma operação — "POST de áudio para um endpoint". Por isso colapsam numa única TranscriptionBackend. Já o faster-whisper é amarrado a uma lib de ML Python sem equivalente limpo em Node: um port TS ou abandonaria STT local ou faria shell-out para um sidecar Python. A matriz escolhe IGNORE/sidecar e diz isso explicitamente. Clonar a estrutura portável, ignorar o que não traduz, e registrar a decisão — é o método de fusão (Lições 03 e 21) operando linha a linha.

CLOUD vs LOCAL · por que um colapsa numa porta e o outro é IGNORADO (comparativo)
CLOUD · 5 provedores Groq OpenAI Whisper Mistral Voxtral xAI Grok STT ElevenLabs Scribe 1 TranscriptionBackend "POST áudio p/ endpoint" disposição: ADAPT (cloud) LOCAL · faster-whisper lib de ML Python · baixa ~150 MB GPU · sem equivalente limpo em Node ✗ não vira kernel TS portável disposição: IGNORE / sidecar Mesma capacidade ("transcrever"), dois caminhos, dois vereditos — porque portabilidade ≠ utilidade. Fonte: docs/hermes-complete-map.md §3.5 · "Portability: Medium · ADAPT cloud, IGNORE/sidecar local".

08 · As 20 provas — o que cada teste fixa

A confiança neste kernel não é fé: são 20 casos em três blocos describe, todos offline (fake backend ou fake fetch), todos sem socket. Veja como eles se distribuem — e repare que cada modo de falha do desenho tem um teste correspondente.

Bloco describeCasosO que prova
transcribe (37–94)7sucesso por URL e por base64; rejeita nenhuma fonte / ambas / URL inválida na fronteira; repassa err do backend; rejeita resultado sem text
analyzeImage (96–138)5sucesso; rejeita prompt vazio (sem chamar backend) / URL inválida; repassa err do backend; rejeita resultado sem analysis
fetch backends (146–263)8mapeia {text,provider} e {analysis}; fallbacks transcript/content; não-2xx / throw de rede / JSON inválido → err; campo numérico coagido a ''
DISTRIBUIÇÃO DOS 20 CASOS · por bloco (cada quadrado = 1 teste)
transcribe 7 casos analyzeImage 5 casos fetch backends 8 casos 7 + 5 + 8 = 20 · todos hermético, $0, sem rede — fake backend ou fake fetch injetado. Defesa em profundidade testada ponta a ponta.
Você já viu o padrão sete vezes. Memory, learning, curator, clarify, web, skills, media — todo subsistema entregue do @alembic/hermes obedece à mesma disciplina: injetar as portas, retornar Result, validar entrada não confiável com Zod, nunca lançar, sem Date.now()/Math.random(). É a Lição 5 tornada concreta, sete vezes. Releia a Lição 5 agora e ela deve soar como um resumo de tudo acima.
SETE SUBSISTEMAS, UMA DISCIPLINA · o mesmo desenho repetido
memory learning curator clarify web skills media ports injetados · Result<T,Error> · Zod na fronteira · nunca lança · sem Date.now()/Math.random() media é o 7º — e fecha a prova de que a disciplina escala sem exceções.

09 · Como isso se encaixa

Estas duas ferramentas não vivem sozinhas — elas são dois dos sentidos do agente no mundo: ouvidos (transcribe) e olhos (analyzeImage). Quando o motor precisa virar uma fala em texto ou descrever uma imagem, são elas que atendem, sempre pela porta injetada. Veja a peça (destacada) no lugar dela na máquina: o que a alimenta à esquerda, o que ela alimenta à direita — e, embaixo, o caminho que a matriz deliberadamente deixou de fora.

A PEÇA NO FLUXO · types/contracts define a forma → fetch-backends resolve a rede → o KERNEL de mídia despacha+re-valida → o texto/análise abastece o loop do agente, a memória e os gates · (o ML local fica IGNORADO embaixo)
contracts · media/types.ts Result + as 2 portas de mídia (a forma · Lição 05) fetch-backends.ts a impl que resolve a rede (POST áudio/imagem · fetch injetável) define injeta ESTA PEÇA · media.ts transcribe / analyzeImage despacha pela porta · XOR/AND re-valida (Zod) · Result texto/análise validados loop do agente · harness raciocina sobre o que ouviu/viu (Lição 19 · o swarm) memória semântica a transcrição vira conhecimento (Lição 07 · MemoryStore) pipeline de gates o err da peça falha fechado (Lição 17 · Proof/Validator) faster-whisper (ML local) ✗ IGNORADO/sidecar — não vira porta (seção 07) deixado de fora ↓ a mesma disciplina de ports-and-injection (Lição 05) sustenta TODA esta fileira — web (11) é o gêmeo deste padrão A peça é um despachante de mídia: não escolhe o provedor nem decide o que fazer com o texto — só garante que o que entra no motor é válido (ou um err honesto). Troque o backend (Groq→OpenAI→ElevenLabs) e nada à direita muda: o contrato fino (a cintura, Lição 14) protege o downstream.
Clique para acender peça a peça — do que alimenta a porta (contracts + fetch-backends) ao que ela abastece (agente, memória, gates).
Onde você está na metodologia. No mapa do curso, esta é a sétima e última das sete portas do agente (subsistemas 07–13) — a que dá ao motor ouvidos e olhos. Ela fica rio acima do trabalho real: o loop do agente (Lição 19) precisa entender uma fala ou imagem, esta peça entrega o texto/análise validado, a memória (Lição 07) o arquiva, e se a peça devolve err a pipeline de gates (Lição 17) falha fechado em vez de raciocinar sobre lixo. Tudo isso só funciona porque ela segue a disciplina de portas & injeção (Lição 05) — o mesmo gabarito que o subsistema web (Lição 11) também encarna, e o mesmo critério de portabilidade que decide o que a fusão (Lição 12 · skills) clona ou ignora. Veja o trajeto inteiro no mapa interativo da metodologia.

10 · Na prática

Honestidade primeiro: não há um comando alembic transcribe nem alembic analyze-image. O kernel desta lição (media.ts) é uma biblioteca dentro de @alembic/hermes — o motor o chama por dentro. Mas o repositório tem comandos de mídia reais, em pacotes irmãos, que exercitam exatamente a mesma forma (porta injetada → kernel → Result): ocr (texto em imagem), vision-index (descrição de imagem) e notes (transcrição → notas de reunião). Todos offline e $0 por padrão; --online liga um backend real. [uncertain] nenhum deles é o transcribe/analyzeImage de @alembic/hermes em si — para isolar este kernel, o caminho direto é a suíte do pacote (mais abaixo).

Leia texto dentro de uma imagem — offline, determinístico, sem rede (o gêmeo de OCR do balcão desta lição):

# @alembic/ocr · offline $0 por padrão; --online precisa de docs/ocr-setup.md
alembic ocr ./fixtures/recibo.png
# ocr: ./fixtures/recibo.png
# <texto reconhecido, determinístico offline>
#
# --online liga o backend SGLang (ALEMBIC_OCR_BASEURL, ex. http://127.0.0.1:10000);
# sem essa env ele FALHA FECHADO com mensagem acionável — nenhuma rede é tentada.

Descreva as imagens de uma família do wiki — append-only, deduplicado por imagem, e nunca escrevendo no corpus (a disciplina de proveniência da Lição 26):

# @alembic/vision · descreve cada imagem do pacote → <data-dir>/vision-index/<family>.jsonl
alembic vision-index ~/Documents/Resources/Bookmarks
# vision-index: Bookmarks (~/Documents/Resources/Bookmarks)
#   packages: 128, images: 412, described: 412, rows written: 397, backend: offline
#   index: ~/.alembic/vision-index/Bookmarks.jsonl
#
# offline = determinístico ($0). --online usa o MLX-VLM local (docs/vision-setup.md).
# Descrições vazias (falha por-imagem) NÃO são gravadas → retentáveis no próximo run.

Transcrição → notas estruturadas. O notes consome uma transcrição já em texto e extrai título, decisões e action items — o passo rio abaixo do transcribe desta lição:

# @alembic/hermes meeting-notes · offline; a data padrão vem do relógio do CLI
alembic notes ./fixtures/reuniao.txt
# notes: Sync de produto (2026-06-27)
#   summary: Offline deterministic meeting-notes placeholder (no model called).
#   participants: (none)
#   decisions: 0 | action items: 0
#
# offline é um placeholder determinístico (nenhum modelo chamado); --online preenche de verdade.

As três saídas acima são o formato real impresso por apps/cli/src/commands.ts (runOcr, runVisionIndex, runNotes); os números/textos são ilustrativos. Para a baseline do monorepo (CLAUDE.md): pnpm -r typecheck && pnpm -r build && pnpm -w test.

E para isolar exatamente este kernel — transcribe/analyzeImage de @alembic/hermes — rode a suíte do pacote. Os 20 casos da seção 08 injetam um backend (ou fetch) fake e nunca abrem socket:

# roda só o pacote @alembic/hermes — onde vive src/media/ (media.ts, fetch-backends.ts)
pnpm --filter @alembic/hermes test
# …
# ✓ src/media/media.test.ts  > transcribe ok por URL e por base64       (fake backend)
# ✓ src/media/media.test.ts  > nenhuma/ambas fonte → err, backend não chamado (XOR na fronteira)
# ✓ src/media/media.test.ts  > analyzeImage rejeita prompt vazio        (AND na fronteira)
# ✓ src/media/media.test.ts  > non-2xx / throw de rede / JSON inválido → err (fetch FALSO)
# ✓ src/media/media.test.ts  > campo numérico coagido a '' antes do Zod (defesa em profundidade)
# Test Files  1 passed
# Tests      20 passed   ← as 20 provas desta lição, todas sem rede
Experimente · prove os caminhos de mídia sem rede (e veja o err honesto)
1
Entre no repo e rode a suíte do pacote. cd na raiz do monorepo e pnpm --filter @alembic/hermes test. Termina em segundos e não abre rede — os testes de packages/hermes/src/media/media.test.ts injetam um backend (ou fetch) fake.
2
Veja o XOR falhar na fronteira. Procure o caso "nenhuma fonte" (media.test.ts:50-60): o teste afirma called === false — o backend nunca é chamado porque a refinação Zod já reprovou o pedido. É a tabela-verdade da seção 03 virando prova executável.
3
Rode um comando de mídia de verdade, offline. Aponte alembic ocr para qualquer PNG no disco (ou alembic notes para um .txt com uma transcrição). Sem --online a saída é determinística e $0; observe o formato ocr: <caminho> seguido do texto — o mesmo desenho "valida → backend → valida → Result" desta lição, num pacote irmão.
4
Confirme a baseline. Rode pnpm -r typecheck && pnpm -r build && pnpm -w test na raiz. O typecheck é a metade silenciosa: ele recusa compilar se você ler result.value sem ter tratado !result.ok antes.
O que você acabou de provar: a forma de 4 estágios — validar pedido, backend injetado, re-validar, Result — roda idêntica em mídia, sem nunca abrir um socket; e os comandos irmãos (ocr/vision-index/notes) provam que o mesmo gabarito vira CLI real e offline. E quando o motor usa o kernel de verdade?
Numa execução real, é o harness que chama transcribe/analyzeImage por dentro de uma unidade da missão — você dispara o motor com alembic run --goal GOAL.md --plan alembic.plan.ts --yes, e quando uma unidade precisa entender áudio/imagem, esta peça atende pela porta injetada. [uncertain] não há um comando alembic transcribe que isole este kernel; para isolá-lo, o caminho direto é o pnpm --filter @alembic/hermes test acima — e os comandos ocr/vision-index/notes são as portas de CLI de mídia que o repositório expõe hoje.

Fixe os conceitos (flashcards)

Clique pra virar. Tente lembrar a resposta antes de virar — recuperação ativa fixa mais que reler.

Forma
Quais os 4 estágios do kernel de mídia?
clique pra virar ↻
Resposta
validar pedido (Zod) → backend injetado → validar resultado (Zod) → retornar Result. Três caminhos para err, nunca lança.
XOR
O que (a===undefined) !== (b===undefined) impõe?
clique pra virar ↻
Resposta
Exatamente uma de audioUrl/audioBase64. É um XOR: nenhuma e ambas falham com err na fronteira.
Envelope
Por que success/error somem do payload?
clique pra virar ↻
Resposta
Colapsam no Result (ok/err). Assim ok({text:''}) (silêncio) ≠ err (falha) — estruturalmente distintos.
Matriz
Por que faster-whisper é IGNORADO?
clique pra virar ↻
Resposta
ML Python (modelo ~150 MB, GPU), sem equivalente Node. Os 5 provedores cloud colapsam em 1 porta; o local é IGNORE/sidecar.

Revisão cumulativa — recupere de memória

Antes de clicar: responda de cabeça. As quatro opções têm o mesmo tamanho de propósito — sem pista pela forma.

1. Um pedido de transcrição fornece ambos audioUrl e audioBase64. O que acontece?
Correto: c. A refinação é um XOR: (audioUrl===undefined) !== (audioBase64===undefined) é verdadeiro só com exatamente uma fonte; ambas → falseerr. a/b erram porque o backend nunca é chamado (teste 62–70 confirma) — a decisão é da fronteira, não do provedor. d erra a forma de falha: o kernel retorna err (um Result), nunca lança.
2. Por que success/error não aparecem como campos em TranscriptionResult?
Correto: b. O success/error do dict plano viram o ok/err do Result; um transcript vazio (silêncio) é ok({text:''}), nunca confundido com falha. a inverte: é uma escolha de design, não um esquecimento. c é factualmente falso — a fonte tem success/error (é justamente o que se colapsa). d erra o motivo: o ganho é semântico (silêncio ≠ falha), não de tamanho.
3. Por que o caminho local faster-whisper foi marcado IGNORE na fusão?
Correto: d. A matriz (§3.5: "ADAPT cloud, IGNORE/sidecar local") decide por portabilidade: o ML Python não vira um kernel TS sem dependências, então é IGNORE/sidecar; o cloud — só "POST de áudio para um endpoint" — vira uma TranscriptionBackend. a erra o critério (não é velocidade, é portabilidade — o local pode até ser mais rápido). b é falso: transcrição e vision são portas distintas. c é falso: é uma decisão deliberada e registrada, não um descuido.
💬 Travou em algo? Eu sou seu professor neste curso — pergunte. "Por que o mapeador coage para '' em vez de falhar?", "Como eu plugaria o Groq de verdade?", "O que muda em analyzeImage versus transcribe?". É só dizer.