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.
