No iadots.com, eu reúno vídeos sobre IA, agentes, robótica e humanoides em quatro idiomas. Assistir a cada vídeo e escrever um resumo à mão não escala, então montei uma automação simples: um script no Google Apps Script pega a transcrição do vídeo pela API da Supadata, manda o texto para um modelo de linguagem e salva o resumo. Neste tutorial mostro como montar a mesma estrutura do zero, com o código completo e os cuidados que aprendi rodando isso em produção.
O exemplo usa uma planilha do Google como fila de vídeos. No meu caso a fonte são as postagens do Blogger, mas a lógica é a mesma.
Pré-requisitos
- Uma conta Google, para usar o Apps Script e o Google Sheets.
- Uma chave de API da Supadata (o plano gratuito serve para testar).
- Uma chave da API do Gemini, gerada no Google AI Studio. Pode ser outro modelo, desde que você adapte a função de resumo.
- Uma planilha com uma aba chamada
Videose três colunas:youtubeId(o código depois dev=na URL),lang(idioma original do vídeo, comoenoupt) eresumo(vazia no começo).
Crie o projeto em Extensões > Apps Script, dentro da própria planilha, ou em script.google.com.
Passo 1: guardar as chaves em Script Properties
Nunca deixe chave de API escrita no código. No editor do Apps Script, vá em Configurações do projeto > Propriedades do script e crie duas propriedades: SUPADATA_API_KEY e GEMINI_API_KEY. O script lê esses valores em tempo de execução:
const SUPADATA_BASE = 'https://api.supadata.ai/v1';
const GEMINI_MODEL = 'gemini-3.8-flash'; // troque pelo modelo que preferir
const PLANILHA_ID = 'COLE_AQUI_O_ID_DA_PLANILHA';
const LIMITE_BYTES = 180000;
function chave(nome) {
const valor = PropertiesService.getScriptProperties().getProperty(nome);
if (!valor) throw new Error('Propriedade ausente: ' + nome);
return valor;
}
Passo 2: a função de transcrição
O endpoint é GET /v1/transcript, autenticado pelo cabeçalho x-api-key. Uso três parâmetros importantes: mode=native, que busca apenas a legenda que já existe no vídeo, sem gerar transcrição por IA; text=true, que devolve o texto corrido em vez de trechos com tempo; e lang, com o idioma original.
Um detalhe da documentação muda tudo: se o idioma pedido não existir, a API devolve o primeiro idioma disponível. Por isso o script confere o campo lang da resposta e descarta o resultado quando ele não bate com o pedido. Assim um vídeo em japonês nunca recebe, por engano, um resumo feito a partir de uma legenda traduzida.
Vídeos muito longos podem voltar com status 202 e um jobId. Nesse caso é só consultar GET /v1/transcript/{jobId} até o status virar completed ou failed.
function buscarTranscricao(youtubeId, lang) {
const key = chave('SUPADATA_API_KEY');
const videoUrl = 'https://www.youtube.com/watch?v=' + youtubeId;
const url = SUPADATA_BASE + '/transcript?url=' + encodeURIComponent(videoUrl) +
'&mode=native&text=true&lang=' + encodeURIComponent(lang);
const r = UrlFetchApp.fetch(url, { muteHttpExceptions: true, headers: { 'x-api-key': key } });
const code = r.getResponseCode();
let data = null;
if (code === 200) data = JSON.parse(r.getContentText());
if (code === 202) data = aguardarJob(JSON.parse(r.getContentText()).jobId, key);
if (!data) return { ok: false, motivo: 'sem_transcricao_http_' + code };
const idioma = String(data.lang || '').toLowerCase().split('-')[0];
const texto = typeof data.content === 'string' ? data.content.trim() : '';
if (idioma !== lang || !texto) return { ok: false, motivo: 'idioma_ou_texto_indisponivel' };
if (Utilities.newBlob(texto).getBytes().length > LIMITE_BYTES) {
return { ok: false, motivo: 'texto_muito_extenso' };
}
return { ok: true, texto: texto };
}
function aguardarJob(jobId, key) {
for (let i = 0; i < 12; i++) {
Utilities.sleep(5000);
const r = UrlFetchApp.fetch(SUPADATA_BASE + '/transcript/' + encodeURIComponent(jobId),
{ muteHttpExceptions: true, headers: { 'x-api-key': key } });
if (r.getResponseCode() !== 200) continue;
const job = JSON.parse(r.getContentText());
if (job.status === 'completed') return job;
if (job.status === 'failed') return null;
}
return null;
}
O limite de 180.000 bytes existe porque transcrições enormes deixam o resumo lento e caro, e podem estourar o tempo de execução do Apps Script.
Passo 3: a função de resumo
Para o resumo uso o método generateContent da API do Gemini, chamado direto pelo UrlFetchApp. A chave vai no cabeçalho x-goog-api-key, não na URL, para não aparecer em logs.
function gerarResumo(transcricao, lang) {
const url = 'https://generativelanguage.googleapis.com/v1beta/models/' +
GEMINI_MODEL + ':generateContent';
const payload = {
systemInstruction: { parts: [{ text: 'Você resume transcrições de vídeos de tecnologia de forma fiel, sem inventar informações.' }] },
contents: [{
role: 'user',
parts: [{ text: 'Escreva um resumo de 3 a 5 frases, no idioma "' + lang +
'", desta transcrição:\n\n' + transcricao }]
}],
generationConfig: { temperature: 0.3 }
};
const r = UrlFetchApp.fetch(url, {
method: 'post',
contentType: 'application/json',
headers: { 'x-goog-api-key': chave('GEMINI_API_KEY') },
payload: JSON.stringify(payload),
muteHttpExceptions: true
});
if (r.getResponseCode() !== 200) {
throw new Error('Gemini HTTP ' + r.getResponseCode() + ': ' + r.getContentText().slice(0, 300));
}
const data = JSON.parse(r.getContentText());
const parts = (((data.candidates || [])[0] || {}).content || {}).parts || [];
const resumo = parts.filter(p => !p.thought).map(p => p.text || '').join('').trim();
if (!resumo) throw new Error('Resposta do Gemini sem texto');
return resumo;
}
Pedir o resumo no idioma original mantém cada versão do site coerente.
Passo 4: juntar tudo e evitar gasto duplicado
Este é o ponto que mais me preocupava. Se a chamada à Supadata expirar no meio do caminho, o script não sabe se o crédito foi gasto. Se ele simplesmente tentar de novo na próxima execução, pode pagar duas vezes pelo mesmo vídeo.
A solução é registrar a tentativa no PropertiesService antes de fazer a requisição. Se a marca já existe, o vídeo é pulado. O registro é atualizado no fim com o resultado, seja sucesso ou motivo da falha.
function listarPendentes() {
const aba = SpreadsheetApp.openById(PLANILHA_ID).getSheetByName('Videos');
return aba.getDataRange().getValues().slice(1)
.map((l, i) => ({
linha: i + 2,
youtubeId: String(l[0]).trim(),
lang: String(l[1]).trim().toLowerCase(),
resumo: String(l[2]).trim()
}))
.filter(v => v.youtubeId && v.lang && !v.resumo);
}
function processarVideo(video) {
const props = PropertiesService.getScriptProperties();
const marca = 'TX_DONE_' + video.youtubeId + '_' + video.lang;
if (props.getProperty(marca)) return; // já tentado: nunca gasta duas vezes
const registro = { at: Date.now(), status: 'consultando' };
props.setProperty(marca, JSON.stringify(registro)); // marca ANTES da requisição
try {
const t = buscarTranscricao(video.youtubeId, video.lang);
if (!t.ok) { registro.status = t.motivo; return; }
const resumo = gerarResumo(t.texto, video.lang);
SpreadsheetApp.openById(PLANILHA_ID).getSheetByName('Videos')
.getRange(video.linha, 3).setValue(resumo);
registro.status = 'resumido';
} catch (e) {
registro.status = 'erro';
registro.erro = String(e).slice(0, 200);
} finally {
props.setProperty(marca, JSON.stringify(registro));
}
}
Se o resumo falhar depois da transcrição, o vídeo fica marcado com erro. Você confere o motivo e apaga a propriedade manualmente quando quiser tentar de novo. É uma decisão consciente: prefiro revisar um caso à mão a pagar duas vezes sem perceber.
Passo 5: gatilho automático
Por fim, uma função principal processa poucos vídeos por vez e um gatilho de tempo a executa a cada hora. O LockService impede que duas execuções rodem ao mesmo tempo e peguem o mesmo vídeo.
function rodarAutomacao() {
const lock = LockService.getScriptLock();
if (!lock.tryLock(1000)) return;
try {
listarPendentes().slice(0, 5).forEach(processarVideo);
} finally {
lock.releaseLock();
}
}
function criarGatilho() {
ScriptApp.getProjectTriggers()
.filter(t => t.getHandlerFunction() === 'rodarAutomacao')
.forEach(t => ScriptApp.deleteTrigger(t));
ScriptApp.newTrigger('rodarAutomacao').timeBased().everyHours(1).create();
}
Execute criarGatilho uma vez pelo editor e autorize as permissões. A partir daí, cada linha nova na planilha ganha um resumo em até uma hora.
Dicas e erros comuns
-
Status 206 ou "transcript-unavailable": o vídeo não tem legenda no idioma pedido. Com
mode=native, isso é esperado. Se quiser cobrir esses casos, testemode=auto, que gera a transcrição por IA quando não há legenda, mas confira antes o custo em créditos. -
Código de idioma errado: use códigos ISO 639-1 (
en,pt,es). O script já ignora sufixos como-BRna comparação. - Tempo de execução: o Apps Script tem limite de alguns minutos por execução. Por isso processo cinco vídeos por rodada.
-
Limite das propriedades: o
PropertiesServicetem cota de armazenamento. Guarde só o status nele e deixe o resumo na planilha ou no seu banco.
Conclusão
Com pouco mais de cem linhas de Apps Script, sem servidor e sem custo de infraestrutura, dá para transformar uma lista de vídeos em resumos prontos para publicar. É assim que o iadots.com mantém resumos de vídeos em quatro idiomas sem trabalho manual. Os pontos que fazem diferença são conferir o idioma devolvido, limitar o tamanho do texto e marcar cada tentativa antes de gastar um crédito.
Teste a Supadata: https://supadata.ai/r/HHD1MQYC
Aviso: este artigo contém um link de indicação da Supadata; posso receber créditos se você se cadastrar por ele.
Top comments (0)