Ferramenta de transcrição interna: construir ou comprar, e depois construir
apiferramentas internasdesenvolvedores

Ferramenta de transcrição interna: construir ou comprar, e depois construir

BMMamane B. MoussaMay 26, 2026Updated July 2, 202615 min read

Summarize this article with:

Construir ou comprar, com honestidade

Construir uma ferramenta de transcrição interna compensa quando sua equipe tem volume suficiente para que os custos do SaaS por usuário superem claramente o que uma API medida e algumas semanas de tempo de engenharia custariam. Esse ponto de virada geralmente ocorre por volta de 10 a 15 usuários ativos. Abaixo disso, pagar pelo Otter ou Fireflies é quase sempre a escolha certa. Acima disso, você ganha vantagens reais: seus dados ficam nos seus próprios sistemas, o acesso é controlado pelo seu SSO existente e você pode construir exatamente o fluxo de trabalho que sua equipe precisa, em vez de se adaptar ao produto de outra pessoa.

Este guia aborda a arquitetura, o cálculo entre construir e comprar, e o código essencial para uma ferramenta interna de nível de produção.

Quando comprar ainda vence

Antes de escrever qualquer código, seja honesto sobre o que você está realmente comprando quando paga por usuário.

A captura automática por bot de reunião é a coisa mais difícil de replicar. Otter e Fireflies enviam um bot para suas chamadas do Zoom ou Meet automaticamente, sem ninguém precisar lembrar de clicar em gravar. Construir isso exige integrar uma API de recall de terceiros, gerenciar o ciclo de vida do bot, lidar com requisitos de GDPR/aviso legal e sobreviver às mudanças nas APIs das plataformas de reunião. Isso representa semanas de trabalho e manutenção contínua.

SOC 2 e postura de conformidade são outro custo real. Quando clientes corporativos perguntam se sua ferramenta interna está em conformidade, a questão passa a ser se sua equipe quer assumir essa auditoria. Ferramentas prontas vêm com seus próprios documentos de conformidade; sua ferramenta interna não.

Os casos genuínos em que construir supera comprar:

  • Você tem um alto volume de arquivos de áudio que não vêm de reuniões (entrevistas, gravações de campo, ligações de clientes, podcasts, depoimentos jurídicos).
  • Você precisa de integração profunda com seus sistemas internos (gestão de casos, CRM, repositório de documentos) que nenhuma integração nativa cobre.
  • Você tem requisitos rigorosos de residência de dados que exigem que o áudio permaneça na sua própria infraestrutura.
  • Sua equipe é grande o suficiente para que os custos por usuário se acumulem de forma significativa.

O Otter.ai Business custa US$ 19,99 por usuário por mês na cobrança anual (ou US$ 30 mensais). O Fireflies Business custa US$ 19 por usuário por mês na cobrança anual (ou US$ 29 mensais). Com 30 usuários, isso dá aproximadamente US$ 570–600 por mês. Uma API medida como a Deepgram Nova-3 custa US$ 0,0077 por minuto para áudio pré-gravado. Uma equipe de 30 pessoas, cada uma enviando uma hora de áudio por mês, paga cerca de US$ 14 em taxas da API de transcrição. A diferença é real.

Otter BusinessFireflies BusinessFerramenta interna (API)
10 usuáriosUS$ 200/mêsUS$ 190/mêsCusto da API + overhead de engenharia
30 usuáriosUS$ 600/mêsUS$ 570/mês~US$ 50–100/mês em volume típico
50 usuáriosUS$ 1.000/mêsUS$ 950/mês~US$ 80–160/mês em volume típico
SSO/SAMLApenas EnterpriseApenas EnterpriseVocê constrói
Bots de reuniãoSim, nativoSim, nativoRequer API de terceiros
Residência de dadosInfra do fornecedorInfra do fornecedorSua infra

Preços por usuário verificados em otter.ai/pricing e fireflies.ai/pricing, julho de 2026. As estimativas de custo da API usam Deepgram Nova-3 a US$ 0,0077/min para áudio pré-gravado (verificado em deepgram.com/pricing), sem incluir hospedagem nem armazenamento.

O alvo da construção: enviar, processar, entregar, reter conforme política
O alvo da construção: enviar, processar, entregar, reter conforme política

A Arquitetura

A ferramenta interna mínima útil tem quatro partes:

  1. Endpoint de upload. Os arquivos vão para seu armazenamento de objetos (S3, R2 ou GCS).
  2. Integração com API de transcrição. Envie trabalhos, receba a conclusão via webhook ou polling.
  3. Banco de dados. Armazene transcrições, metadados dos trabalhos e atribuição de usuários.
  4. Busca e painel. Encontre transcrições por usuário, projeto, conteúdo ou data.

Escolhas de stack que mantêm tudo simples:

  • Frontend: Next.js ou seu framework interno existente (Retool também funciona para prototipagem rápida).
  • Backend: serviço REST em Node.js, Python ou Go.
  • Banco de dados: Postgres com busca full-text embutida.
  • Armazenamento: S3 ou R2 para arquivos de áudio.
  • Autenticação: Google OAuth primeiro, depois SAML/OIDC quando o TI pedir.

O Modelo de Dados

Três tabelas principais cobrem o caso de uso essencial:

CREATE TABLE users (
  id UUID PRIMARY KEY,
  email TEXT UNIQUE NOT NULL,
  role TEXT NOT NULL DEFAULT 'member',
  department TEXT,
  created_at TIMESTAMPTZ DEFAULT NOW()
);

CREATE TABLE transcription_jobs (
  id UUID PRIMARY KEY,
  user_id UUID REFERENCES users(id),
  audio_filename TEXT NOT NULL,
  audio_s3_key TEXT NOT NULL,
  external_job_id TEXT UNIQUE,
  status TEXT NOT NULL,
  language TEXT,
  duration_seconds INT,
  project TEXT,
  tags TEXT[],
  created_at TIMESTAMPTZ DEFAULT NOW(),
  completed_at TIMESTAMPTZ
);

CREATE TABLE transcripts (
  job_id UUID PRIMARY KEY REFERENCES transcription_jobs(id),
  full_text TEXT NOT NULL,
  metadata JSONB,
  summary TEXT,
  search_vector tsvector GENERATED ALWAYS AS (
    to_tsvector('english', full_text)
  ) STORED
);

CREATE INDEX idx_search ON transcripts USING gin(search_vector);
CREATE INDEX idx_jobs_user ON transcription_jobs(user_id);
CREATE INDEX idx_jobs_project ON transcription_jobs(project);

A coluna tsvector oferece busca full-text do Postgres em todo o corpus de transcrições sem adicionar outro serviço. Para corpora muito grandes (centenas de milhares de transcrições), vale considerar Meilisearch ou Elasticsearch.

Adicione user_id aos trabalhos desde o início. Seis meses depois, você vai querer saber quem enviou o quê. Adicionar isso retroativamente é doloroso.

O Fluxo de Upload

O frontend coleta o arquivo mais os metadados (projeto, idioma, tags) e envia um POST multipart. O backend faz upload para o S3 e depois envia à sua API de transcrição:

import express from 'express';
import multer from 'multer';
import { S3Client, PutObjectCommand } from '@aws-sdk/client-s3';

const app = express();
const upload = multer({ storage: multer.memoryStorage() });
const s3 = new S3Client({ region: process.env.AWS_REGION });

app.post('/api/transcribe', requireAuth, upload.single('audio'), async (req, res) => {
  const { project, tags, language } = req.body;
  const user = req.user;

  const s3Key = `audio/${user.id}/${Date.now()}-${req.file.originalname}`;
  await s3.send(new PutObjectCommand({
    Bucket: process.env.S3_BUCKET,
    Key: s3Key,
    Body: req.file.buffer,
    ContentType: req.file.mimetype,
  }));

  const audioUrl = `https://${process.env.S3_BUCKET}.s3.amazonaws.com/${s3Key}`;

  // Replace with whichever transcription API you choose.
  // This example uses a URL-based submission (no re-upload needed).
  const apiRes = await fetch('https://api.example-transcription.com/v1/transcribe', {
    method: 'POST',
    headers: {
      'Authorization': `Bearer ${process.env.TRANSCRIPTION_API_KEY}`,
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({
      source: 'url',
      input_url: audioUrl,
      language: language || 'auto',
    }),
  });

  const { job_id: externalJobId } = await apiRes.json();

  const result = await db.query(
    `INSERT INTO transcription_jobs
     (user_id, audio_filename, audio_s3_key, external_job_id, status, language, project, tags)
     VALUES ($1, $2, $3, $4, 'queued', $5, $6, $7) RETURNING id`,
    [user.id, req.file.originalname, s3Key, externalJobId,
     language || 'auto', project, tags ? JSON.parse(tags) : []]
  );

  res.status(201).json({ job_id: result.rows[0].id });
});

Manipulador de Webhooks

Webhooks superam o polling em experiência do usuário e carga do servidor. Quando a API de transcrição termina, ela faz um POST no seu endpoint com uma assinatura que você verifica antes de confiar no payload.

O nome exato do cabeçalho e o formato da assinatura variam conforme a API. Verifique-os na documentação do fornecedor escolhido antes de escrever o manipulador. Um padrão genérico que funciona na maioria:

app.post('/api/webhooks/transcription',
  express.raw({ type: 'application/json' }),
  async (req, res) => {
    // Verify HMAC signature, header name varies by vendor.
    // Common patterns: X-Webhook-Signature, X-Signature-256, etc.
    const sigHeader = req.header('X-Webhook-Signature') || '';
    const sigValue = sigHeader.replace('sha256=', '');
    const expected = crypto
      .createHmac('sha256', process.env.WEBHOOK_SECRET)
      .update(req.body)
      .digest('hex');

    if (!crypto.timingSafeEqual(Buffer.from(sigValue), Buffer.from(expected))) {
      return res.status(401).send('Invalid signature');
    }

    const event = JSON.parse(req.body.toString());

    if (event.event === 'job.completed') {
      const { job_id: externalJobId, result_url, duration } = event.data;

      // Fetch the actual transcript from the result URL.
      const transcriptRes = await fetch(result_url, {
        headers: { 'Authorization': `Bearer ${process.env.TRANSCRIPTION_API_KEY}` },
      });
      const transcriptData = await transcriptRes.json();

      await db.query(
        `UPDATE transcription_jobs
         SET status = 'completed', duration_seconds = $1, completed_at = NOW()
         WHERE external_job_id = $2`,
        [duration, externalJobId]
      );

      await db.query(
        `INSERT INTO transcripts (job_id, full_text, metadata, summary)
         SELECT id, $1, $2, $3 FROM transcription_jobs WHERE external_job_id = $4
         ON CONFLICT (job_id) DO UPDATE
           SET full_text = EXCLUDED.full_text,
               metadata = EXCLUDED.metadata,
               summary = EXCLUDED.summary`,
        [
          transcriptData.text,
          JSON.stringify(transcriptData.metadata || {}),
          transcriptData.summary || null,
          externalJobId,
        ]
      );
    }

    res.sendStatus(200);
  }
);

Duas coisas para acertar aqui: recusar o processamento quando a assinatura estiver ausente ou inválida (não processe o payload) e responder com 200 rapidamente. Qualquer processamento lento deve ser transferido para uma fila. Para uma comparação mais aprofundada dos trade-offs entre webhook e polling, veja webhook vs polling para transcrições.

Busca Full-Text

Busca full-text do Postgres em todo o seu corpus, com trechos das correspondências:

app.get('/api/search', requireAuth, async (req, res) => {
  const { q, project, user_id, from, to } = req.query;

  const result = await db.query(`
    SELECT
      j.id, j.audio_filename, j.created_at, j.project,
      u.email AS user_email,
      ts_headline(
        'english', t.full_text,
        plainto_tsquery('english', $1),
        'StartSel=**, StopSel=**, MaxWords=30'
      ) AS snippet
    FROM transcripts t
    JOIN transcription_jobs j ON j.id = t.job_id
    JOIN users u ON u.id = j.user_id
    WHERE t.search_vector @@ plainto_tsquery('english', $1)
      AND ($2::text IS NULL OR j.project = $2)
      AND ($3::uuid IS NULL OR j.user_id = $3::uuid)
      AND ($4::timestamptz IS NULL OR j.created_at >= $4)
      AND ($5::timestamptz IS NULL OR j.created_at <= $5)
    ORDER BY ts_rank(t.search_vector, plainto_tsquery('english', $1)) DESC
    LIMIT 50
  `, [q, project || null, user_id || null, from || null, to || null]);

  res.json(result.rows);
});

O índice GIN em search_vector mantém isso rápido em um corpus de dezenas de milhares de transcrições. O trecho mostra a correspondência em contexto, e é isso que torna a busca realmente utilizável.

Controle de Acesso

Dois padrões cobrem a maioria das equipes.

Por departamento: cada usuário vê apenas as transcrições do seu departamento. Adicione uma coluna department à tabela de trabalhos e filtre toda consulta pelo departamento do usuário solicitante. Simples e difícil de configurar errado.

Por projeto: os usuários pertencem a projetos e veem todas as transcrições dos seus projetos. Mais flexível, requer uma tabela de junção:

CREATE TABLE user_projects (
  user_id UUID REFERENCES users(id),
  project TEXT NOT NULL,
  role TEXT NOT NULL DEFAULT 'member',
  PRIMARY KEY (user_id, project)
);

Toda consulta então faz join com user_projects para aplicar a visibilidade. Aplique isso no servidor, nunca no código cliente. Uma flag no frontend é uma dica, não um portão.

Para equipes acima de 20 pessoas, comece a planejar o SSO. A maioria dos departamentos de TI eventualmente exigirá que qualquer ferramenta interna que manipule áudio sensível se integre ao seu provedor de identidade (Okta, Azure AD, Google Workspace). Construa o armazenamento de usuários desacoplado do método de autenticação desde o início, para poder adicionar um adaptador SAML depois sem reestruturar o modelo de dados.

A Interface do Painel

A solução de 90% precisa de cinco telas:

  • Formulário de upload com campos de projeto, tags e idioma.
  • Lista de trabalhos mostrando transcrições recentes, status e ações rápidas (visualizar, baixar, excluir).
  • Visualização da transcrição com texto completo, rótulos de falantes, marcações de tempo e botões de copiar/baixar.
  • Página de busca com consulta full-text e filtros suspensos por projeto, usuário e intervalo de datas.
  • Visão administrativa mostrando uso por usuário e departamento, para atribuição de custos e dimensionamento de planos.

Retool ou Internal.io podem entregar o painel em uma tarde se você já tiver a API. Um Next.js customizado leva uma semana se você quiser uma interface polida que colegas não técnicos consigam usar confortavelmente.

Retenção de Dados e Custos de Armazenamento

Defina uma política de retenção antes de lançar, não depois. Arquivos de áudio são grandes; transcrições são minúsculas. Uma entrevista de 60 minutos a 128kbps tem aproximadamente 60MB. Uma transcrição em texto da mesma entrevista fica abaixo de 100KB.

Um padrão prático: manter o áudio por 30 a 90 dias (tempo suficiente para as pessoas voltarem e conferirem, curto o bastante para manter os custos de armazenamento controlados) e manter as transcrições indefinidamente. Em contextos de conformidade jurídica, de saúde ou financeira, pode ser necessária uma retenção de áudio mais longa. Construa o período de retenção como um campo configurável no registro do workspace ou departamento, para poder definir políticas diferentes para equipes diferentes.

Nunca dependa de exclusão manual. Escreva um job de limpeza que rode em agendamento e exclua os arquivos de áudio cujo período de retenção tenha expirado.

Para o modelo de custos da ferramenta interna se sustentar, o armazenamento precisa continuar barato. Cloudflare R2 e Backblaze B2 são ambos consideravelmente mais baratos que o S3 para armazenamento puro e egress. Vale considerar se o volume de áudio for alto.

Recursos Que Valem a Pena Adicionar Depois

Notificações no Slack ou Notion. Publique automaticamente um resumo em um canal do projeto quando uma transcrição for concluída. Para o Slack, uma URL de webhook por canal é simples e não exige revisão de app. Veja integração de transcrição com Slack e integração de transcrição com Notion.

Logs de auditoria. Quem acessou qual transcrição e quando. Importante para setores regulados e útil em qualquer organização. Adicione uma tabela access_log que registre leituras, não apenas escritas.

Modelos de saída personalizados. Execute seus próprios prompts sobre o texto da transcrição para gerar saídas específicas da empresa (notas de reunião estruturadas, resumos de casos, análises de chamadas de vendas) além do que a API de transcrição retorna. Veja os padrões de processamento em lote em transcrição em lote para grandes projetos.

Cadastro de voz para rótulos de falantes. Se sua API suporta diarização de falantes, você pode melhorar a precisão dos rótulos cadastrando amostras curtas de voz dos membros da equipe. Isso transforma "Speaker 0 / Speaker 1" em nomes reais automaticamente.

2FA com TOTP ou chave física. Quando você tiver áudio sensível na sua ferramenta interna, a camada de autenticação importa. Adicione um segundo fator antes que a ferramenta manipule algo verdadeiramente confidencial.

Armadilhas Comuns

Pular o banco de dados em projetos "simples". Um arquivo JSON ou planilha funciona em escala minúscula e se torna impossível com 50 transcrições. Use Postgres desde o primeiro dia. Só a busca full-text já vale a pena.

Não rastrear user_id nos trabalhos desde o início. Seis meses depois, você vai querer saber quem transcreveu o quê para atribuição de custos, conformidade ou depuração. Adicionar isso retroativamente significa uma migração e uma lacuna nos dados históricos.

Falta de rastreamento de idioma. Equipes multilíngues precisam disso. Capture no envio, armazene no trabalho, inclua nos filtros de busca. A precisão da transcrição varia significativamente por idioma entre diferentes APIs.

Construir você mesmo a captura automática por bot de reunião. Isso é mais difícil do que parece. Se seu caso de uso principal é gravar chamadas do Zoom ou Meet, avalie Otter ou Fireflies primeiro, mesmo que construa um wrapper em cima deles depois.

Assumir que a transcrição vem no payload do webhook. A maioria das APIs de produção (incluindo as modeladas aqui) envia uma result_url no webhook, não o texto completo da transcrição. Seu manipulador precisa buscar o resultado como um segundo passo após verificar a assinatura.

O Próximo Passo Prático

Levante a versão mínima (upload + manipulador de webhooks + visualização da transcrição) em um dia. Coloque três ou quatro membros da equipe usando antes de adicionar busca ou polir o painel. Os primeiros usuários reais dizem o que construir em seguida.

Se você só precisa de uma transcrição limpa sem construir nada, o ConvertAudioToText processa uploads de arquivos diretamente no navegador, sem necessidade de conta para um teste rápido.

Perguntas Frequentes

Quanto custa manter uma ferramenta de transcrição interna em comparação com pagar por usuário?

Com 30 usuários, o Otter.ai Business custa cerca de US$ 600/mês e o Fireflies Business cerca de US$ 570/mês. Uma ferramenta interna construída sobre uma API medida (como a Deepgram Nova-3 a US$ 0,0077/min) transfere o custo para o uso real. Uma equipe com média de uma hora de áudio por pessoa por mês paga cerca de US$ 14 em taxas de API para 30 usuários, mais sua hospedagem e armazenamento. O ponto de virada em que construir compensa normalmente ocorre por volta de 10 a 15 usuários ativos.

Quais padrões de controle de acesso funcionam melhor para ferramentas de transcrição de equipe?

Dois padrões cobrem a maioria dos casos. Por departamento: cada usuário vê apenas as transcrições do seu departamento, aplicado por meio de uma coluna de departamento na tabela de trabalhos e consultas filtradas. Por projeto: os usuários podem transcrever para qualquer projeto a que pertencem, aplicado por meio de uma tabela de junção. O modelo por projeto é mais flexível e mais fácil de expandir depois. Ambos exigem aplicação no lado do servidor, não flags no cliente.

Preciso de SSO para construir uma ferramenta de transcrição interna?

Não no início, mas planeje para isso. A maioria das equipes de TI bloqueará uma nova ferramenta interna acima de 20 usuários se ela exigir uma senha separada. Construa sua camada de autenticação de modo que o armazenamento de usuários fique desacoplado do método de autenticação desde o primeiro dia. Google OAuth é rápido de lançar e atende à maioria das equipes. SAML/OIDC (para Okta, Azure AD etc.) pode ser adicionado depois como um adaptador, sem tocar no modelo de dados.

Como devo lidar com a retenção de arquivos de áudio para controlar os custos de armazenamento?

Defina uma política desde o primeiro dia, não depois. Arquivos de áudio são grandes; transcrições são minúsculas. Um padrão prático: manter o áudio por 30 a 90 dias e as transcrições indefinidamente. Para setores regulados (saúde, jurídico), pode ser necessária uma retenção de áudio mais longa, então torne o período de retenção configurável por workspace ou departamento. Nunca dependa de exclusão manual.

Fontes

Try transcription free

Convert any audio or video to clean, unwatermarked text — speaker labels, timestamps, and AI summaries included. First 10 minutes free, no account.

Related Articles