Herramienta de transcripción interna: construir o comprar, y luego construir
apiherramientas internasdesarrolladores

Herramienta de transcripción interna: construir o comprar, y luego construir

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

Summarize this article with:

Construir o comprar, con honestidad

Crear una herramienta de transcripción interna compensa cuando tu equipo maneja suficiente volumen como para que los costos del SaaS por usuario superen claramente lo que costaría una API de pago por uso más unas semanas de tiempo de ingeniería. Ese punto de equilibrio suele situarse alrededor de 10-15 usuarios activos. Por debajo, pagar por Otter o Fireflies es casi siempre la decisión correcta. Por encima, obtienes ventajas reales: tus datos permanecen en tus propios sistemas, el acceso se controla con tu SSO existente y puedes construir exactamente el flujo de trabajo que tu equipo necesita en lugar de adaptarte al producto de otra persona.

Esta guía cubre la arquitectura, el análisis de construir frente a comprar y el código esencial para una herramienta interna de nivel producción.

Cuándo comprar sigue ganando

Antes de escribir cualquier código, sé honesto sobre lo que realmente estás comprando cuando pagas por usuario.

La captura automática con bots de reuniones es lo más difícil de replicar. Otter y Fireflies envían un bot a tus llamadas de Zoom o Meet automáticamente, sin que nadie tenga que acordarse de pulsar grabar. Construir eso requiere integrar una API de terceros de recall de reuniones, gestionar el ciclo de vida del bot, cumplir los requisitos de RGPD/aviso legal y sobrevivir a los cambios de las API de las plataformas de reuniones. Eso son semanas de trabajo y mantenimiento continuo.

La certificación SOC 2 y el cumplimiento normativo son otro costo real. Cuando clientes empresariales preguntan si tu herramienta interna cumple con las normas, la pregunta se convierte en si tu equipo quiere asumir esa auditoría. Las herramientas comerciales vienen con su propia documentación de cumplimiento; tu herramienta interna no.

Los casos reales en los que construir gana a comprar:

  • Tienes un alto volumen de archivos de audio que no provienen de reuniones (entrevistas, grabaciones de campo, llamadas de clientes, pódcasts, declaraciones judiciales).
  • Necesitas una integración profunda con tus sistemas internos (gestión de casos, CRM, almacén de documentos) que ninguna integración nativa cubre.
  • Tienes requisitos estrictos de residencia de datos que exigen que el audio permanezca en tu propia infraestructura.
  • Tu equipo es lo suficientemente grande como para que los costos por usuario se acumulen de forma significativa.

Otter.ai Business cuesta $19.99 por usuario al mes facturado anualmente (o $30 mensual). Fireflies Business cuesta $19 por usuario al mes facturado anualmente (o $29 mensual). Con 30 usuarios, son unos $570-600 al mes. Una API de pago por uso como Deepgram Nova-3 cuesta $0.0077 por minuto para audio pregrabado. Un equipo de 30 personas donde cada una envía una hora de audio al mes paga unos $14 en tarifas de la API de transcripción. La diferencia es real.

Otter BusinessFireflies BusinessHerramienta interna (API)
10 usuarios$200/mes$190/mesCosto de API + sobrecarga de ingeniería
30 usuarios$600/mes$570/mes~$50-100/mes con volumen típico
50 usuarios$1,000/mes$950/mes~$80-160/mes con volumen típico
SSO/SAMLSolo plan EnterpriseSolo plan EnterpriseLo construyes tú
Bots de reunionesSí, nativoSí, nativoRequiere API de terceros
Residencia de datosInfraestructura del proveedorInfraestructura del proveedorTu infraestructura

Precios por usuario verificados en otter.ai/pricing y fireflies.ai/pricing, julio de 2026. Las estimaciones de costo de API usan Deepgram Nova-3 a $0.0077/min para audio pregrabado (verificado en deepgram.com/pricing), sin incluir alojamiento ni almacenamiento.

El objetivo de construcción: subir, procesar, entregar y conservar según la política
El objetivo de construcción: subir, procesar, entregar y conservar según la política

La arquitectura

La herramienta interna mínima útil tiene cuatro partes:

  1. Endpoint de subida. Los archivos van a tu almacenamiento de objetos (S3, R2 o GCS).
  2. Integración con la API de transcripción. Envías trabajos y recibes la finalización vía webhook o sondeo.
  3. Base de datos. Almacena transcripciones, metadatos de trabajos y atribución de usuarios.
  4. Búsqueda y panel. Encuentra transcripciones por usuario, proyecto, contenido o fecha.

Elecciones de stack que lo mantienen simple:

  • Frontend: Next.js o tu framework actual de herramientas internas (Retool también sirve para prototipado rápido).
  • Backend: servicio REST en Node.js, Python o Go.
  • Base de datos: Postgres con búsqueda de texto completo integrada.
  • Almacenamiento: S3 o R2 para los archivos de audio.
  • Autenticación: primero Google OAuth y luego SAML/OIDC cuando TI lo pida.

El modelo de datos

Tres tablas principales cubren el caso de uso esencial:

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);

La columna tsvector te da búsqueda de texto completo de Postgres en todo el corpus de transcripciones sin añadir otro servicio. Para corpus muy grandes (cientos de miles de transcripciones), vale la pena considerar Meilisearch o Elasticsearch.

Añade user_id a los trabajos desde el principio. A los seis meses querrás saber quién subió qué. Añadirlo retroactivamente es doloroso.

El flujo de subida

El frontend recoge el archivo más los metadatos (proyecto, idioma, etiquetas) y envía un POST multipart. El backend lo sube a S3 y luego lo envía a tu API de transcripción:

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 });
});

Manejador de webhooks

Los webhooks superan al sondeo en experiencia de usuario y carga del servidor. Cuando la API de transcripción termina, hace un POST a tu endpoint con una firma que verificas antes de confiar en el payload.

El nombre exacto del encabezado y el formato de la firma varían según la API. Verifícalos en la documentación del proveedor que elijas antes de escribir el manejador. Un patrón genérico que funciona en la mayoría:

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);
  }
);

Dos cosas que debes hacer bien aquí: fallar de forma segura ante una firma ausente o inválida (no procesar el payload) y responder con un 200 rápidamente. Cualquier procesamiento lento debe delegarse a una cola. Para una comparación más profunda de las ventajas y desventajas entre webhook y sondeo, consulta webhook frente a sondeo para transcripciones.

Búsqueda de texto completo

Búsqueda de texto completo de Postgres en todo tu corpus, con fragmentos de coincidencia:

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);
});

El índice GIN sobre search_vector mantiene esto rápido con un corpus de decenas de miles de transcripciones. El fragmento muestra la coincidencia en contexto, que es lo que hace que la búsqueda sea realmente utilizable.

Control de acceso

Dos patrones cubren a la mayoría de los equipos.

Alcance por departamento: cada usuario solo ve las transcripciones de su departamento. Añade una columna department a la tabla de trabajos y filtra cada consulta por el departamento del usuario que la solicita. Simple y difícil de configurar mal.

Alcance por proyecto: los usuarios pertenecen a proyectos y ven todas las transcripciones de sus proyectos. Más flexible, requiere una tabla intermedia:

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)
);

Cada consulta se une entonces contra user_projects para aplicar la visibilidad. Aplica esto en el servidor, nunca en el código del cliente. Una bandera en el frontend es una pista, no una barrera.

Para equipos de más de 20 personas, empieza a planificar el SSO. La mayoría de los departamentos de TI acabarán exigiendo que cualquier herramienta interna que maneje audio sensible se integre con tu proveedor de identidad (Okta, Azure AD, Google Workspace). Construye el almacén de usuarios desacoplado del método de autenticación desde el principio, para poder añadir un adaptador SAML más adelante sin reestructurar el modelo de datos.

La interfaz del panel

La solución del 90% necesita cinco vistas:

  • Formulario de subida con campos de proyecto, etiquetas e idioma.
  • Lista de trabajos que muestra las transcripciones recientes, el estado y acciones rápidas (ver, descargar, eliminar).
  • Vista de transcripción con el texto completo, etiquetas de hablantes, marcas de tiempo y botones de copiar/descargar.
  • Página de búsqueda con consulta de texto completo y menús desplegables de filtro por proyecto, usuario y rango de fechas.
  • Vista de administrador que muestra el uso por usuario y departamento, para atribución de costos y dimensionamiento de planes.

Retool o Internal.io pueden darte el panel en una tarde si ya tienes la API. Un Next.js a medida lleva una semana si quieres una interfaz pulida que compañeros no técnicos puedan usar cómodamente.

Retención de datos y costos de almacenamiento

Define una política de retención antes de lanzar, no después. Los archivos de audio son grandes; las transcripciones son diminutas. Una entrevista de 60 minutos a 128kbps pesa unos 60MB. Una transcripción en texto de la misma entrevista ocupa menos de 100KB.

Un valor predeterminado práctico: conserva el audio durante 30-90 días (suficiente para que la gente pueda volver a revisarlo, pero corto para mantener controlados los costos de almacenamiento) y conserva las transcripciones indefinidamente. En contextos de cumplimiento legal, sanitario o financiero, puede requerirse una retención de audio más larga. Construye el período de retención como un campo configurable en el registro del espacio de trabajo o departamento, para poder establecer políticas diferentes para distintos equipos.

Nunca dependas del borrado manual. Escribe un trabajo de limpieza que se ejecute de forma programada y elimine los archivos de audio cuyo período de retención haya expirado.

Para que el modelo de costos de la herramienta interna se sostenga, el almacenamiento debe seguir siendo barato. Cloudflare R2 y Backblaze B2 son ambos considerablemente más baratos que S3 para almacenamiento puro y egreso. Vale la pena considerarlos si el volumen de audio es alto.

Cosas que vale la pena añadir después

Notificaciones de Slack o Notion. Publica automáticamente un resumen en un canal del proyecto cuando se completa una transcripción. Para Slack, una URL de webhook por canal es simple y no requiere revisión de aplicación. Consulta integrar la transcripción con Slack y integrar la transcripción con Notion.

Registros de auditoría. Quién accedió a qué transcripción y cuándo. Importante para industrias reguladas y útil en toda organización. Añade una tabla access_log que registre lecturas, no solo escrituras.

Plantillas de salida personalizadas. Ejecuta tus propios prompts sobre el texto de la transcripción para obtener resultados específicos de la empresa (notas de reunión estructuradas, resúmenes de casos, análisis de llamadas de ventas) más allá de lo que devuelve la API de transcripción. Consulta los patrones de procesamiento por lotes en transcripción por lotes para proyectos grandes.

Registro de voz para etiquetas de hablantes. Si tu API admite diarización de hablantes, puedes mejorar la precisión de las etiquetas registrando muestras cortas de voz de los miembros del equipo. Esto convierte "Speaker 0 / Speaker 1" en nombres reales automáticamente.

2FA con TOTP o llave de hardware. Cuando tengas audio sensible en tu herramienta interna, la capa de autenticación importa. Añade un segundo factor antes de que la herramienta maneje algo verdaderamente confidencial.

Errores comunes

Omitir la base de datos en proyectos "simples". Un archivo JSON o una hoja de cálculo funciona a escala muy pequeña y se vuelve imposible con 50 transcripciones. Usa Postgres desde el primer día. La búsqueda de texto completo ya justifica por sí sola la decisión.

No registrar user_id en los trabajos desde el principio. A los seis meses querrás saber quién transcribió qué para atribución de costos, cumplimiento normativo o depuración. Añadirlo retroactivamente implica una migración y un hueco en los datos históricos.

Falta de seguimiento del idioma. Los equipos multilingües lo necesitan. Captúralo al enviar, guárdalo en el trabajo e inclúyelo en los filtros de búsqueda. La precisión de la transcripción varía significativamente según el idioma entre las distintas APIs.

Construir tú mismo la captura automática con bots de reuniones. Es más difícil de lo que parece. Si tu caso de uso principal es grabar llamadas de Zoom o Meet, evalúa primero Otter o Fireflies, aunque luego construyas un wrapper sobre ellos.

Asumir que la transcripción viene en el payload del webhook. La mayoría de las APIs de producción (incluidas las modeladas aquí) envían una result_url en el webhook, no el texto completo de la transcripción. Tu manejador necesita obtener el resultado como segundo paso después de verificar la firma.

El siguiente paso práctico

Monta la versión mínima (subida + manejador de webhooks + vista de transcripción) en un día. Consigue que tres o cuatro miembros del equipo la usen antes de añadir refinamientos de búsqueda o del panel. Los primeros usuarios reales te dirán qué construir después.

Si solo necesitas una transcripción limpia sin construir nada, ConvertAudioToText gestiona la subida de archivos directamente en el navegador, sin necesidad de cuenta, para una prueba rápida.

Preguntas frecuentes

¿Cuánto cuesta mantener una herramienta de transcripción interna en comparación con pagar por usuario?

Con 30 usuarios, Otter.ai Business cuesta unos $600/mes y Fireflies Business unos $570/mes. Una herramienta interna basada en una API de pago por uso (como Deepgram Nova-3 a $0.0077/min) traslada el costo al uso real. Un equipo que promedia una hora de audio por persona al mes paga unos $14 en tarifas de API para 30 usuarios, más tu alojamiento y almacenamiento. El punto de equilibrio en el que construir gana suele darse alrededor de 10-15 usuarios activos.

¿Qué patrones de control de acceso funcionan mejor para herramientas de transcripción de equipo?

Dos patrones cubren la mayoría de los casos. Por departamento: cada usuario solo ve las transcripciones de su departamento, aplicado mediante una columna de departamento en la tabla de trabajos y consultas filtradas. Por proyecto: los usuarios pueden transcribir a cualquier proyecto al que pertenezcan, aplicado mediante una tabla intermedia. El modelo por proyecto es más flexible y más fácil de ampliar después. Ambos requieren aplicación en el servidor, no banderas en el cliente.

¿Necesito SSO para crear una herramienta de transcripción interna?

No al principio, pero planifícalo. La mayoría de los equipos de TI bloquearán una nueva herramienta interna por encima de 20 usuarios si requiere una contraseña separada. Construye tu capa de autenticación de modo que el almacén de usuarios esté desacoplado del método de autenticación desde el primer día. Google OAuth es rápido de implementar y cubre a la mayoría de los equipos. SAML/OIDC (para Okta, Azure AD, etc.) puede añadirse después como adaptador sin tocar el modelo de datos.

¿Cómo debo gestionar la retención de archivos de audio para controlar los costos de almacenamiento?

Define una política desde el primer día, no después. Los archivos de audio son grandes; las transcripciones son diminutas. Un valor predeterminado práctico: conserva el audio durante 30-90 días y las transcripciones indefinidamente. En industrias reguladas (salud, legal), puede requerirse una retención de audio más larga, así que haz que el período de retención sea configurable por espacio de trabajo o departamento. Nunca dependas del borrado manual.

Fuentes

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