Como transcrever podcast por API, do RSS ao acervo inteiro
O enclosure do feed já é uma URL pública, então o podcast é o caso mais fácil de transcrever. O trabalho está no acervo antigo, no nome dos convidados e no que fazer com o texto.
Podcast é o áudio mais fácil de transcrever por API, e por um motivo bobo: o
feed RSS já entrega uma URL pública para cada episódio. Não tem upload, não tem
bucket, não tem arquivo local. O enclosure do item é exatamente o que o campo
url da requisição espera.
curl https://api.transcrevo.com/v1/transcripts \
-H "Authorization: Bearer $TRANSCREVO_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"url": "https://cdn.exemplo.com/ep-142.mp3",
"speakers": true,
"language": "pt",
"glossary": { "gatu": "Gatto", "bertual": "Bertuol" },
"webhookUrl": "https://seu-servidor.com/hooks/transcrevo"
}'O resto do post é sobre o que fazer em volta disso: o acervo antigo, os nomes próprios e o texto que sai.
O acervo antigo, num laço
Um podcast semanal de três anos tem uns 150 episódios. A quarenta minutos cada, são 100 horas de áudio. A US$ 0,03 por hora, US$ 3,00 para o acervo inteiro. Com separação de falantes, US$ 5,00.
O laço é direto, e o único cuidado é o teto de 120 criações por minuto:
const API = "https://api.transcrevo.com";
const headers = {
Authorization: `Bearer ${process.env.TRANSCREVO_API_KEY}`,
"Content-Type": "application/json",
};
for (const episode of episodes) {
const res = await fetch(`${API}/v1/transcripts`, {
method: "POST",
// A chave de idempotência é o id do episódio: repetir o script não paga de novo.
headers: { ...headers, "Idempotency-Key": `ep-${episode.guid}` },
body: JSON.stringify({
url: episode.enclosure,
speakers: true,
language: "pt",
glossary: episode.glossary,
webhookUrl: "https://seu-servidor.com/hooks/transcrevo",
}),
});
if (res.status === 429) {
await new Promise((r) => setTimeout(r, Number(res.headers.get("Retry-After") ?? 5) * 1000));
continue;
}
const { data } = await res.json();
await save(episode.guid, data.id);
}O Idempotency-Key é o que separa um script que você pode rodar de novo de um
script que cobra duas vezes. Mesma chave, mesmo corpo, devolve a mesma
transcrição, quantas vezes você repetir. Chave repetida com corpo diferente
devolve 409 idempotency_conflict, que é o aviso de que a chave foi
reaproveitada por engano.
O 429 rate_limited traz Retry-After com a espera real em segundos. Um acervo
inteiro destravando de uma vez encosta no teto com facilidade; espalhar a rajada
pela espera é mais simples que qualquer fila. Os limites estão em
preços e limites, e os códigos em erros.
O nome dos convidados é onde o texto erra
Nome próprio, marca e anglicismo são o erro mais comum de qualquer sistema de transcrição. Não é falha específica: são as palavras que menos aparecem em qualquer treino. E podcast é feito disso. Todo episódio tem um convidado com sobrenome que o sistema nunca viu, uma empresa citada quinze vezes, um termo do nicho que só existe naquele meio.
Você sabe disso antes de mandar o áudio. O campo glossary é a correção, e ela
vale só para aquele episódio:
{
"url": "https://cdn.exemplo.com/ep-142.mp3",
"glossary": {
"mote gruto": "Mateus Gruto",
"cubernetes": "Kubernetes",
"fintéquis": "fintechs"
}
}Até 100 entradas, de até 80 caracteres cada. A correção vale por sequência de palavras, então trocar duas palavras por duas funciona, e o intervalo de tempo é repartido entre as palavras trocadas. Na prática, o glossário de um podcast é duas listas: uma fixa, com os nomes dos apresentadores e do programa, e uma por episódio, com o convidado da vez. A referência completa está em transcrições.
O que fazer com o transcrito
O texto corrido serve para a página do episódio e para busca. O que vale mais
está em words, que traz cada palavra com start e end em segundos decimais.
- Busca dentro do episódio. Indexar as palavras com o timestamp permite que o resultado da busca leve o ouvinte para o minuto certo, e não para o episódio inteiro. É a diferença entre "este episódio fala sobre isso" e "fala sobre isso aos 34min12".
- Corte para clipe. O trecho que virou clipe de trinta segundos é um par de timestamps. O erro mediano do timestamp de palavra fica entre 69 e 86 ms, então cortar na palavra é seguro.
- Capítulo. Uma pausa longa entre turnos costuma ser troca de assunto. Não é perfeito, mas é um rascunho melhor que a página em branco.
O que não confiar sem revisão
Publicar transcrito sem ler é decisão de custo, e ela precisa de números. Os nossos: 6,34% de WER em 2.000 trechos de fala espontânea brasileira, medido contra transcrição humana. Em cem palavras, seis erradas. Para busca e para descoberta, isso não atrapalha. Para publicar como texto oficial do episódio, alguém lê.
Dois casos específicos de podcast:
- Música de fundo às vezes vira texto. Vinheta cantada e trilha com letra são transcritas como fala. Se o seu programa tem abertura musical, os primeiros segundos do transcrito vão precisar de corte.
- Fala sobreposta recebe um rótulo só. Apresentador e convidado rindo por cima um do outro viram um falante. A diarização tem DER de 14,8% nessa fatia, contra 4,6% no geral. Podcast de duas pessoas com microfone individual é o cenário bom; mesa de quatro pessoas num microfone só é o cenário ruim. O detalhamento está em precisão e qualidade, e o que a separação de falantes faz e não faz está em o que é diarização.
Episódio novo, no dia da publicação
Quem publica semanalmente não roda script de acervo: roda um gancho. O feed
atualiza, você pega o enclosure do item novo e cria a transcrição com
webhookUrl. Quando o aviso chega, você busca o transcrito uma vez pelo id e
publica junto com o episódio.
O corpo do webhook traz id, status, durationSeconds e cost, mas não traz
text nem words: num episódio de duas horas esses campos passam de 10 MB, e
webhook não é lugar de trafegar isso. Cada entrega vem assinada no header
Transcrevo-Signature, com o segredo de webhook da conta (whsec_...), que o
painel mostra e rotaciona sem mexer nas chaves de API.
Um episódio de quarenta minutos custa US$ 0,03 com falantes, ou US$ 0,02 sem. É menos que o armazenamento do próprio mp3.