
Outil de transcription interne : développer ou acheter, puis construire
Summarize this article with:
Développer vs acheter, en toute franchise
Créer un outil de transcription interne devient rentable quand votre équipe a un volume suffisant pour que les coûts SaaS par utilisateur dépassent largement ce qu'une API à l'usage et quelques semaines d'ingénierie coûteraient. Ce seuil se situe généralement autour de 10 à 15 utilisateurs actifs. En dessous, payer Otter ou Fireflies reste presque toujours le choix logique. Au-dessus, vous gagnez de vrais avantages : vos données restent sur vos systèmes, l'accès est géré par votre SSO existant, et vous bâtissez exactement le flux de travail dont votre équipe a besoin au lieu de vous plier au produit d'un tiers.
Ce guide détaille l'architecture, le calcul du choix développer vs acheter, et le code essentiel pour un outil interne prêt pour la production.
Quand l'achat reste la meilleure option
Avant d'écrire la moindre ligne de code, regardez ce que vous achetez avec un abonnement par utilisateur.
La capture automatique par bot de réunion est la brique la plus difficile à reproduire. Otter et Fireflies envoient automatiquement un bot dans vos appels Zoom ou Meet, sans que personne n'ait besoin de penser à enregistrer. Développer cela exige d'intégrer une API tierce de bots d'enregistrement (type Recall), de gérer leur cycle de vie, de respecter le RGPD et les avertissements légaux, et de s'adapter aux changements d'API des plateformes de visioconférence. Cela représente des semaines de travail et une maintenance continue.
La conformité et la posture SOC 2 constituent un autre coût bien réel. Si des clients entreprise demandent si votre outil interne est conforme, votre équipe devra porter la responsabilité de cet audit. Les solutions du marché fournissent leur propre documentation de conformité ; votre outil interne ne le fait pas.
Les cas concrets où développer surpasse l'achat :
- Vous traitez un fort volume de fichiers audio qui ne proviennent pas de réunions (entretiens, enregistrements terrain, appels clients, podcasts, dépositions juridiques).
- Vous avez besoin d'une intégration poussée avec vos systèmes internes (gestion de dossiers, CRM, base documentaire) qu'aucun connecteur natif ne gère.
- Vous avez des contraintes strictes de résidence des données imposant que l'audio reste sur votre propre infrastructure.
- Votre équipe est assez grande pour que les coûts par utilisateur deviennent significatifs.
Otter.ai Business coûte 19,99 $ par utilisateur et par mois avec facturation annuelle (ou 30 $ par mois). Fireflies Business coûte 19 $ par utilisateur et par mois avec facturation annuelle (ou 29 $ par mois). Pour 30 utilisateurs, cela représente environ 570 à 600 $ par mois. Une API facturée à l'usage comme Deepgram Nova-3 coûte 0,0077 $ par minute pour de l'audio préenregistré. Une équipe de 30 personnes soumettant chacune une heure d'audio par mois paie environ 14 $ de frais d'API de transcription.
| Otter Business | Fireflies Business | Outil interne (API) | |
|---|---|---|---|
| 10 utilisateurs | 200 $/mois | 190 $/mois | Coût API + maintenance ingé |
| 30 utilisateurs | 600 $/mois | 570 $/mois | ~50-100 $/mois au volume standard |
| 50 utilisateurs | 1 000 $/mois | 950 $/mois | ~80-160 $/mois au volume standard |
| SSO/SAML | Enterprise uniquement | Enterprise uniquement | À votre charge |
| Bots de réunion | Oui, natif | Oui, natif | Requiert une API tierce |
| Résidence des données | Infra du fournisseur | Infra du fournisseur | Votre infra |
Tarifs par siège vérifiés sur otter.ai/pricing et fireflies.ai/pricing en juillet 2026. Les estimations de coût d'API reposent sur Deepgram Nova-3 à 0,0077 $/min en différé (vérifié sur deepgram.com/pricing), hors hébergement et stockage.
La cible d'architecture : téléverser, traiter, livrer, conserver selon la politique
L'architecture
La version minimale utile d'un outil interne comprend quatre éléments :
- Point de terminaison de téléversement. Les fichiers partent sur votre stockage objet (S3, R2 ou GCS).
- Intégration de l'API de transcription. Envoi des tâches, réception de la fin du traitement par webhook ou interrogation périodique.
- Base de données. Stockage des transcriptions, des métadonnées de tâche et de l'attribution aux utilisateurs.
- Recherche et tableau de bord. Retrouver les transcriptions par utilisateur, projet, contenu ou date.
Des choix de stack pour rester simple :
- Frontend : Next.js ou votre framework d'outils internes actuel (Retool convient aussi pour un prototype rapide).
- Backend : service REST en Node.js, Python ou Go.
- Base de données : Postgres avec recherche plein texte native.
- Stockage : S3 ou R2 pour les fichiers audio.
- Authentification : Google OAuth d'abord, puis SAML/OIDC dès que l'IT le demande.
Le modèle de données
Trois tables principales suffisent pour l'usage fondamental :
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 colonne tsvector vous donne la recherche plein texte Postgres sur tout le corpus de transcriptions sans ajouter un autre service. Pour les très grands corpus (des centaines de milliers de transcriptions), Meilisearch ou Elasticsearch méritent d'être étudiés.
Associez un user_id aux tâches dès le départ. Dans six mois, vous voudrez savoir qui a téléversé quoi. L'ajouter rétroactivement est pénible.
Le flux de téléversement
Le frontend récupère le fichier et ses métadonnées (projet, langue, tags), puis envoie une requête POST multipart. Le backend l'envoie sur S3, puis soumet la tâche à votre API de transcription :
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}`;
// Remplacez par l'API de transcription de votre choix.
// Cet exemple utilise une soumission par URL (pas de réexpédition nécessaire).
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 });
});
Le gestionnaire de webhooks
Les webhooks sont préférables à l'interrogation pour l'expérience utilisateur comme pour la charge serveur. Dès que l'API de transcription termine, elle envoie un POST à votre endpoint avec une signature que vous vérifiez avant de traiter la charge utile.
Le nom exact du header et le format de la signature changent selon l'API. Vérifiez-les dans la documentation du fournisseur choisi avant d'écrire le gestionnaire. Voici un modèle générique qui fonctionne dans la plupart des cas :
app.post('/api/webhooks/transcription',
express.raw({ type: 'application/json' }),
async (req, res) => {
// Vérification de signature HMAC, le nom du header varie selon le fournisseur.
// Formats fréquents : 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('Signature invalide');
}
const event = JSON.parse(req.body.toString());
if (event.event === 'job.completed') {
const { job_id: externalJobId, result_url, duration } = event.data;
// Récupération de la transcription finale via l'URL fournie.
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);
}
);
Deux règles d'or ici : refuser immédiatement en cas de signature absente ou invalide (ne traitez pas la charge utile), et renvoyer un statut 200 rapidement. Tout traitement lourd doit être délégué à une file d'attente. Pour une analyse approfondie des compromis webhook contre interrogation périodique, consultez webhook vs polling pour les transcriptions.
Recherche plein texte
Recherche plein texte Postgres sur votre corpus, avec extraits contextuels :
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);
});
L'index GIN sur search_vector garantit des temps de réponse rapides sur un corpus de dizaines de milliers de transcriptions. L'extrait contextualisé met en avant la correspondance, ce qui rend la recherche réellement pratique.
Contrôle d'accès
Deux approches couvrent la majorité des équipes.
Par département : Chaque utilisateur ne voit que les transcriptions de son département. Ajoutez une colonne department à la table des tâches et filtrez chaque requête en fonction du département du demandeur. Simple et très difficile à mal configurer.
Par projet : Les utilisateurs appartiennent à des projets et voient toutes les transcriptions de ces projets. Plus souple, cela nécessite une table de liaison :
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)
);
Chaque requête applique ensuite une jointure sur user_projects pour vérifier la visibilité. Appliquez toujours cette règle côté serveur, jamais dans le code client. Un indicateur dans le front-end n'est qu'une indication d'affichage, pas une sécurité.
Pour les équipes de plus de 20 personnes, préparez le SSO. La majorité des équipes informatiques finira par exiger que tout outil interne manipulant de l'audio confidentiel soit connecté à votre fournisseur d'identité (Okta, Azure AD, Google Workspace). Découplez le stockage des utilisateurs du mécanisme d'authentification dès le départ, pour pouvoir intégrer un connecteur SAML plus tard sans restructurer la base.
L'interface utilisateur
La version utile à 90 % nécessite cinq vues :
- Formulaire de téléversement avec champs projet, tags et langue.
- Liste des tâches affichant les transcriptions récentes, leur statut et des actions rapides (consulter, télécharger, supprimer).
- Vue transcription avec texte complet, étiquettes d'intervenants, horodatages et boutons copier/télécharger.
- Page de recherche avec requête plein texte et filtres déroulants par projet, utilisateur et période.
- Vue d'administration présentant la consommation par utilisateur et département, pour le suivi des coûts et le dimensionnement.
Retool ou Internal.io permettent de monter cette interface en une après-midi si l'API existe déjà. Un développement Next.js sur mesure demande une semaine pour obtenir une interface soignée, adaptée aux profils non techniques.
Rétention des données et coûts de stockage
Établissez une politique de rétention avant le lancement, pas après. L'audio pèse lourd, la transcription presque rien. Un entretien de 60 minutes à 128 kbps représente environ 60 Mo. La transcription texte de ce même enregistrement fait moins de 100 Ko.
Une approche standard équilibrée : garder l'audio pendant 30 à 90 jours (assez pour réécouter et vérifier, assez court pour maîtriser l'espace disque), et conserver les transcriptions indéfiniment. Dans des contextes juridiques, médicaux ou financiers, une durée de rétention plus longue peut être imposée. Intégrez cette durée sous la forme d'un champ paramétrable par espace de travail ou département afin d'adapter les règles selon les équipes.
Ne vous reposez jamais sur des suppressions manuelles. Mettez en place un script de nettoyage planifié qui efface automatiquement les fichiers audio dont le délai de rétention est dépassé.
Pour préserver la rentabilité d'un outil interne, le stockage doit rester économique. Cloudflare R2 et Backblaze B2 sont nettement plus avantageux que S3 en coût brut de stockage comme en frais de sortie. Une option intéressante si vous traitez de gros volumes audio.
Évolutions utiles à ajouter plus tard
Notifications Slack ou Notion. Publier automatiquement un résumé dans un canal projet dès qu'une transcription est prête. Pour Slack, une URL de webhook par canal suffit et évite la validation d'une application. Voir intégrer la transcription avec Slack et intégrer la transcription avec Notion.
Journaux d'audit. Savoir qui a consulté quelle transcription, et quand. C'est indispensable dans les secteurs réglementés et utile dans n'importe quelle structure. Ajoutez une table access_log qui trace les lectures, pas seulement les modifications.
Modèles de sortie personnalisés. Exécutez vos propres prompts sur le texte transcrit pour produire des synthèses propres à l'entreprise (comptes rendus structurés, résumés de dossiers, analyses d'appels commerciaux) au-delà des retours bruts de l'API. Voir les modèles de traitement par lots dans transcription par lots pour les grands projets.
Empreintes vocales pour les intervenants. Si votre API prend en charge la diarisation, vous pouvez fiabiliser l'identification en enregistrant de courts échantillons vocaux des collaborateurs. Cela transforme automatiquement les mentions « Speaker 0 / Speaker 1 » en vrais noms.
2FA par TOTP ou clé matérielle. Dès que l'outil stocke des enregistrements sensibles, la sécurité de l'accès devient prioritaire. Activez un second facteur avant d'y faire transiter des données réellement confidentielles.
Pièges fréquents
Se passer de base de données pour démarrer vite. Un simple fichier JSON ou un tableau fonctionne à toute petite échelle et devient ingérable à partir de 50 transcriptions. Utilisez Postgres dès le départ. La recherche plein texte justifie ce choix à elle seule.
Négliger le suivi de user_id sur les tâches. Dans six mois, vous aurez besoin de savoir qui a transcrit quoi pour refacturer les coûts, répondre aux audits ou résoudre des bugs. L'ajouter plus tard implique une migration complexe et crée un vide sur l'historique.
Oublier d'enregistrer la langue. C'est crucial pour les équipes multilingues. Capturez l'information dès la soumission, stockez-la avec la tâche et ajoutez-la aux filtres de recherche. La qualité de transcription change nettement selon la langue sur différentes API.
Vouloir coder son propre bot de réunion. C'est bien plus difficile qu'il n'y paraît. Si votre besoin principal est d'enregistrer des réunions Zoom ou Meet, testez Otter ou Fireflies en priorité, quitte à créer une surcouche autour d'eux ensuite.
Penser que la transcription se trouve directement dans le webhook. La majorité des API professionnelles (dont celles prises en exemple ici) transmettent une result_url dans le webhook, et non le texte complet. Votre script doit aller chercher le résultat dans une seconde étape, après validation de la signature.
L'étape pratique suivante
Déployez la version minimale (téléversement + gestionnaire webhook + affichage du texte) en une journée. Faites-la tester par trois ou quatre collègues avant d'ajouter la recherche ou d'affiner l'interface. Ce sont les premiers utilisateurs réels qui dicteront les prochaines fonctionnalités.
Si vous voulez simplement une transcription propre sans rien développer, ConvertAudioToText prend en charge vos fichiers directement dans le navigateur. Les 30 premières minutes de n'importe quel fichier sont gratuites sans inscription, et un compte gratuit permet de transcrire jusqu'à 30 fichiers par jour, jusqu'à 30 minutes chacun, la lecture et la copie restant toujours gratuites.
Questions fréquentes
Combien coûte un outil de transcription interne par rapport à un abonnement par utilisateur ?
À 30 utilisateurs, Otter.ai Business revient à environ 600 $/mois et Fireflies Business à environ 570 $/mois. Un outil interne basé sur une API à l'usage (comme Deepgram Nova-3 à 0,0077 $/min) indexe le coût sur l'utilisation réelle. Une équipe avec en moyenne une heure d'audio par personne par mois paie environ 14 $ de frais d'API pour 30 utilisateurs, hors hébergement et stockage. Le seuil de rentabilité se situe généralement autour de 10 à 15 utilisateurs actifs.
Quels modèles de contrôle d'accès fonctionnent le mieux pour un outil d'équipe ?
Deux approches couvrent la majorité des cas. Par département : chaque utilisateur ne voit que les transcriptions de son département, via une colonne dédiée sur la table des tâches et des requêtes filtrées. Par projet : les utilisateurs transcrivent dans n'importe quel projet auquel ils appartiennent, géré par une table de liaison. Le modèle par projet est plus souple et plus simple à faire évoluer. Les deux exigent une validation côté serveur, jamais de simples flags côté client.
Faut-il obligatoirement du SSO pour créer un outil de transcription interne ?
Pas au début, mais anticipez-le. La plupart des équipes IT bloqueront un nouvel outil interne au-delà de 20 utilisateurs s'il nécessite un mot de passe distinct. Concevez votre couche d'authentification pour découpler la table des utilisateurs du mode de connexion dès le départ. Google OAuth est rapide à déployer et couvre la majorité des équipes. SAML/OIDC (Okta, Azure AD, etc.) peut s'ajouter plus tard sous forme d'adaptateur sans toucher au modèle de données.
Comment gérer la rétention des fichiers audio pour maîtriser les coûts de stockage ?
Définissez une politique dès le premier jour. Les fichiers audio sont volumineux, les transcriptions sont minuscules. Un choix par défaut efficace : conserver l'audio de 30 à 90 jours, garder les transcriptions indéfiniment. Pour les secteurs réglementés (santé, juridique), une conservation plus longue peut être requise : rendez donc cette durée configurable par espace de travail ou département. Ne vous fiez jamais à une suppression manuelle.
Sources
- Tarifs Otter.ai (vérifié en juillet 2026)
- Tarifs Fireflies.ai (vérifié en juillet 2026)
- Tarifs Deepgram (vérifié en juillet 2026)
- Tarifs ConvertAudioToText (vérifié en juillet 2026)
Try transcription free
Convert any audio or video to clean, unwatermarked text — speaker labels, timestamps, and AI summaries included. First 30 minutes free, no account.
Related Articles

Speechmatics Alternative for Non-Developers: Web Transcription Without Code
Speechmatics is genuinely excellent for developers: 50 hours free per month, 56 languages, on-prem deployment. If you need a drag-and-drop web app with flat $14.99/mo pricing instead of an API, here is an honest comparison of the two.

Best Transcription Tools with API Access (2026)
Which transcription SaaS tools actually give you API keys, and on which plan? Verified pricing and plan gates for Descript, Sonix, Fireflies, Happy Scribe, AssemblyAI, and more.