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

transcribe / analyzeImage — portas de mídia

Duas ferramentas — speech-to-text 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. Os backends de produção são impls fetch sem dependências. Esta lição é a prova final de que a disciplina escala: uma capacidade nova é uma porta mais um kernel mais uma costura fetch fina — nada mais. Um CLONE do transcription_tools.py (1799 LOC) + vision_tools.py do Hermes, só o caminho CLOUD.

Dois backends, dois kernels, uma forma

A chamada de rede de cada ferramenta vira uma porta injetada — uma única função retornando um Result. O módulo não importa SDK:

// 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>>;
transcribe(req) / analyzeImage(req) validar pedido Zod · cruzado backend injetado fetch (prod) / fake (test) validar resultado Zod · não confiável Result success/error vivem no envelope Result, nunca inline — uma falha nunca se disfarça de texto/análise vazios os seis provedores STT + o caminho local faster-whisper da fonte colapsam para UMA porta injetada (ML local = IGNORADO)
// packages/hermes/src/media/media.ts:51-68 — transcribe
const parsedReq = transcriptionRequestSchema.safeParse(req);
if (!parsedReq.success) return err(new Error(`Invalid transcription request: …`));
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: …`)); // saída não confiável
return ok(parsed.data);

analyzeImage é byte-a-byte a mesma forma (validar pedido → backend → validar resultado), diferindo só nos schemas. Essa simetria é a lição: uma vez estabelecido o padrão, uma nova ferramenta de mídia é mecânica.

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 (um XOR sobre os dois opcionais):

// 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 !== sobre dois checks === undefined é um XOR booleano: verdadeiro só quando exatamente uma fonte está presente. Dois testes fixam ambos os modos de falha — nenhuma fonte, e ambas as fontes, são cada um rejeitados na fronteira antes do backend ser chamado.

O colapso do envelope. A fonte Python retorna dicts planos: {success, transcript, provider, error} para STT, {success, analysis} para vision. Aqui, success/error colapsam no envelope Result, e o núcleo de sucesso é aparado: transcript → o text idiomático do motor (com provider opcional de proveniência), analysis fica analysis. O ganho: uma falha não pode se disfarçar de text vazio — é um err, estruturalmente distinto de ok({text:''}) (silêncio legítimo).

Os backends fetch — mapeamento defensivo, fallbacks de campo

Como o backend web, os backends de mídia são impls fetch finas sobre o fetch global, com mapeamento defensivo de payload e fallbacks de campo. O kernel re-valida, então o mapeador pode ser tolerante:

// packages/hermes/src/media/fetch-backends.ts:163-178 — mapeamento defensivo de linha
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
});
Por que o caminho de ML local é IGNORADO — uma decisão deliberada da matriz

A fonte suporta seis provedores STT cloud e um caminho local faster-whisper (ML Python, território de GPU/download de modelos). A matriz de fusão marca o caminho local como IGNORE: é amarrado a ML Python e fora de escopo para um kernel TypeScript portável, enquanto os seis provedores cloud colapsam para uma porta injetada (todos só fazem POST de áudio para um endpoint). Esta é a disciplina funcionando como pretendido — clonar a estrutura portável, ignorar o que não traduz, e dizer isso explicitamente. Testes injetam um fetch fake provando o mapeamento e os caminhos de transporte falha-fechado (não-2xx, throw de rede, JSON não parseável cada um → err) sem abrir um socket. Um teste até prova que um campo de payload não-string falha fechado pelo portão Zod do kernel — defesa em profundidade, de novo.

1. Um pedido de transcrição fornece ambos audioUrl e audioBase64. O que acontece?
Correto: c. O .refine do schema é um XOR: (audioUrl===undefined) !== (audioBase64===undefined) é verdadeiro só quando exatamente um está presente. Ambos (ou nenhum) falha fechado com err — nunca lança, nunca chega ao backend.
2. Por que success/error não aparecem como campos em TranscriptionResult?
Correto: b. O success/error do envelope plano do Python viram o ok/err do Result. O benefício é que um transcript vazio (silêncio real) é ok({text:''}), nunca confundido com uma falha, que é err.
3. Por que o caminho local faster-whisper de transcrição 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 despacho STT cloud — que é só "POST de áudio para um endpoint" — vira uma porta TranscriptionBackend.

Confusões comuns

"Isto pluga um provedor de transcrição real." Não — é a ESTRUTURA portada para portas-e-injeção. O kernel não importa SDK; createFetchTranscriptionBackend/createFetchVisionBackend são costuras finas de JSON genérico sobre um fetch injetável, então os testes nunca abrem um socket.
"Transcript vazio significa que falhou." Não — um text vazio é legítimo (silêncio) e chega como ok({text:''}). Uma falha é um err. Manter success/error fora do payload e no Result é exatamente o que torna os dois inequívocos.
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(). Isso é a Lição 5 tornada concreta, sete vezes. Releia a Lição 5 agora e ela deve soar como um resumo de tudo acima.