Construire un pipeline RAG à partir de transcriptions vidéo
La plupart des pipelines RAG ne savent pas indexer directement de la vidéo. Voici le chemin complet, d’une URL vidéo jusqu’aux chunks horodatés dans une base vectorielle — avec le code de découpage, le schéma PostgreSQL et la forme exacte renvoyée par l’API.
Avant qu’un modèle de langage puisse chercher, résumer ou citer une vidéo, il faut :
- récupérer le média ;
- en extraire la piste audio ;
- la transcrire ;
- conserver les horodatages ;
- découper la transcription en chunks exploitables ;
- calculer les embeddings ;
- stocker les chunks dans une base vectorielle.
Techtuel prend en charge les trois premières étapes derrière une seule API de transcription.
Envoyez une URL YouTube, une URL de podcast, une URL audio directe ou une URL vidéo. Techtuel résout la source et renvoie un texte structuré, découpé en segments horodatés, que vous pouvez pousser vers PostgreSQL, pgvector, Qdrant, Weaviate, Pinecone ou tout autre moteur de recherche.
URL vidéo
↓
API de transcription Techtuel
↓
Transcription horodatée
↓
Découpage et métadonnées
↓
Embeddings
↓
Base vectorielle
↓
Réponse RAG avec horodatage de la sourcePourquoi le RAG a besoin de transcriptions structurées
Un simple bloc de texte suffit rarement à bâtir une application RAG fiable.
Un pipeline d’ingestion vidéo exploitable doit conserver :
- l’URL de la source ;
- le titre du média ;
- l’instant de départ de chaque segment ;
- la transcription d’origine ;
- la langue ;
- des identifiants stables ;
- assez de contexte autour de chaque chunk.
Les horodatages comptent particulièrement : ils permettent à votre application de renvoyer une réponse accompagnée d’un lien vers le moment exact où l’information apparaît.
Par exemple :
{
"answer": "L’intervenant recommande de séparer l’ingestion de l’indexation.",
"sources": [
{
"title": "Construire un pipeline RAG en production",
"url": "https://www.youtube.com/watch?v=example&t=742s",
"start": 742
}
]
}Sans horodatage, un système RAG peut citer une vidéo de deux heures sans donner à l’utilisateur le moindre moyen de vérifier la réponse.
Transcrire une URL vidéo en un seul appel
Envoyez l’URL de la source à POST /v1/transcribe :
curl https://api.techtuel.com/v1/transcribe \
-H "Authorization: Bearer $TECHTUEL_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"source_url": "https://www.youtube.com/watch?v=VIDEO_ID"
}'La source est résolue, transcrite, et le texte revient dans la réponse — un seul appel, sans identifiant de job à conserver :
{
"id": "job_9f2c",
"status": "completed",
"title": "Construire un pipeline RAG en production",
"language": "fr",
"minutes": 42.5,
"transcript": "Aujourd’hui, nous allons construire...",
"segments": [
{
"start_seconds": 0.0,
"text": "Aujourd’hui, nous allons construire un pipeline RAG de production."
},
{
"start_seconds": 4.2,
"text": "Nous commencerons par l’ingestion, puis le découpage et la recherche."
}
]
}Le corps de la requête n’accepte que quatre champs : source_url, audio_url, upload_id et preferred_language. Le texte complet se trouve dans transcript ; il n’existe pas de champ text au premier niveau.
Une transcription ne peut pas dépendre indéfiniment de la durée d’une requête HTTP : POST /v1/transcribe attend jusqu’à trente secondes (ajustable via ?wait=, jusqu’à 120). Passé ce délai, la réponse devient 202 avec {"id": "job_9f2c", "status": "processing"} — ce n’est pas une erreur, mais le signal de récupérer la transcription plus tard via GET /v1/transcriptions/{id}. C’est le mode de fonctionnement normal pour les médias longs, détaillé plus bas.
Observez la forme d’un segment : start_seconds et text, rien d’autre. L’API ne renvoie pas d’instant de fin, ni d’horodatage au mot près. Un segment court implicitement jusqu’au début du suivant — ce qui suffit à reconstituer un intervalle, à condition de le faire vous-même plutôt que d’attendre un champ qui n’existe pas. Ce contrat d’horodatage est détaillé sur la page API de transcription avec horodatages.
La même intégration vaut pour YouTube, les épisodes de podcast, les sources RSS, les fichiers audio directs et les fichiers vidéo distants.
Le modèle par jobs, quand le média est long ou l’ingestion massive
POST /v1/transcribe couvre le cas courant : une source, une réponse. Dès que vous ingérez une conférence de deux heures, ou tout un catalogue de chaîne YouTube en une passe, le bon outil reste l’API de transcription asynchrone : POST /v1/transcriptions renvoie immédiatement 202 avec un identifiant de job, que votre worker relit à son rythme.
curl https://api.techtuel.com/v1/transcriptions \
-H "Authorization: Bearer $TECHTUEL_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"source_url": "https://www.youtube.com/watch?v=VIDEO_ID"
}'{
"id": "job_9f2c",
"status": "processing"
}Vous interrogez ensuite le job jusqu’à un statut terminal :
curl https://api.techtuel.com/v1/transcriptions/job_9f2c \
-H "Authorization: Bearer $TECHTUEL_API_KEY"Quatre statuts existent : processing, claimed, completed, failed. Seuls completed et failed sont terminaux — claimed signale simplement qu’un worker a pris le job en charge, traitez-le exactement comme processing. Il n’y a pas de webhook : c’est votre worker qui relit ses jobs, soit un point d’entrée public de moins à sécuriser chez vous.
const TERMINAL = new Set(['completed', 'failed'])
export async function waitForTranscription(id: string) {
for (;;) {
const response = await fetch(
`https://api.techtuel.com/v1/transcriptions/${id}`,
{ headers: { Authorization: `Bearer ${process.env.TECHTUEL_API_KEY}` } },
)
const job = await response.json()
// `claimed` n'est PAS terminal : un worker vient juste de prendre le job.
if (TERMINAL.has(job.status)) return job
await new Promise((resolve) => setTimeout(resolve, 5_000))
}
}Une réponse 202 de POST /v1/transcribe se récupère exactement de la même façon : le job est le même objet, sur le même point d’entrée.
Transformer les segments en documents RAG
Ne créez pas systématiquement un vecteur par segment de transcription.
Les segments produits par la reconnaissance vocale sont souvent très courts. Pour la phase de recherche, mieux vaut regrouper des segments adjacents tout en conservant l’instant de départ d’origine — et en déduisant l’instant de fin du segment qui suit.
Un chunk exploitable ressemble à ceci :
{
"id": "video_123_chunk_17",
"content": "Un pipeline d’ingestion de production doit conserver les métadonnées de la source...",
"metadata": {
"source_type": "youtube",
"source_url": "https://www.youtube.com/watch?v=VIDEO_ID",
"title": "Construire un pipeline RAG en production",
"start": 742.4,
"end": 811.7,
"language": "fr"
}
}Ici, le champ end est le vôtre : il est calculé au moment du découpage, à partir du début du segment suivant. Il ne fait pas partie de la réponse de l’API — inutile de le chercher dans segments.
Une stratégie de découpage simple consiste à :
- viser 300 à 800 tokens par chunk ;
- fusionner des segments consécutifs ;
- garder un léger recouvrement entre chunks ;
- ne jamais perdre le premier horodatage d’un chunk ;
- stocker l’URL de la source sur chaque chunk ;
- conserver la transcription brute à part.
La taille idéale dépend de votre modèle d’embedding, de votre stratégie de recherche et de la nature du contenu.
Exemple TypeScript : préparer les chunks de transcription
L’API ne fournit que des instants de départ : le découpage regarde donc un segment en avance pour refermer le chunk précédent. Le dernier chunk n’a pas de successeur, sa fin reste donc ouverte — ou se déduit du champ minutes du job, que l’API renvoie une fois la durée connue.
type Segment = {
start_seconds: number
text: string
}
type TranscriptChunk = {
content: string
start: number
/** Déduit, pas renvoyé par l’API : le début du segment suivant.
* Indéfini sur le dernier chunk, sauf si une durée totale est fournie. */
end?: number
}
export function createTranscriptChunks(
segments: Segment[],
maxCharacters = 2_000,
totalSeconds?: number,
): TranscriptChunk[] {
const usable = segments.filter((segment) => segment.text.trim() !== '')
const chunks: TranscriptChunk[] = []
let current: TranscriptChunk | null = null
usable.forEach((segment, index) => {
const text = segment.text.trim()
// Un segment court jusqu’au démarrage du suivant. Le dernier n’a pas de
// successeur : il reste ouvert et se referme plus bas via totalSeconds.
const next = usable[index + 1]
const segmentEnd = next?.start_seconds
if (!current) {
current = { content: text, start: segment.start_seconds, end: segmentEnd }
return
}
const mergedContent = `${current.content} ${text}`
if (mergedContent.length > maxCharacters) {
chunks.push(current)
current = { content: text, start: segment.start_seconds, end: segmentEnd }
return
}
current.content = mergedContent
current.end = segmentEnd
})
if (current) chunks.push(current)
// Referme le dernier chunk avec la durée facturable du job, si connue.
const last = chunks.at(-1)
if (last && last.end === undefined && totalSeconds !== undefined) {
last.end = totalSeconds
}
return chunks
}Appelez-la avec les minutes du job converties en secondes pour refermer la fin :
const chunks = createTranscriptChunks(job.segments, 2_000, job.minutes * 60)Cet exemple volontairement minimal regroupe les segments adjacents et reconstitue leurs bornes temporelles.
En production, vous pourrez y ajouter :
- des limites exprimées en tokens ;
- un découpage sémantique ;
- une détection des fins de phrase ;
- des frontières de locuteur ;
- un recouvrement entre chunks ;
- des règles spécifiques à la langue.
Stocker les transcriptions dans PostgreSQL et pgvector
Un schéma minimal sépare le média source de ses chunks indexables.
Comme l’API ne renvoie que des instants de départ, end_seconds est une valeur que votre pipeline déduit — elle est donc nullable : le dernier chunk d’un média n’a légitimement aucun successeur pour le refermer.
CREATE TABLE media_sources (
id UUID PRIMARY KEY,
external_id TEXT UNIQUE NOT NULL,
source_url TEXT NOT NULL,
title TEXT,
language TEXT,
transcript TEXT NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE TABLE transcript_chunks (
id UUID PRIMARY KEY,
media_source_id UUID NOT NULL REFERENCES media_sources(id) ON DELETE CASCADE,
content TEXT NOT NULL,
-- start_seconds vient directement du segment renvoyé par l’API.
start_seconds DOUBLE PRECISION NOT NULL,
-- end_seconds est DÉDUIT du début du segment suivant ; NULL sur le dernier
-- chunk, qui n’a pas de successeur pour le refermer.
end_seconds DOUBLE PRECISION,
embedding VECTOR(1536),
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);Votre couche de recherche peut alors combiner :
- la similarité vectorielle ;
- la recherche plein texte PostgreSQL ;
- la recherche hybride ;
- le filtrage sur métadonnées ;
- le reranking ;
- la génération de sources horodatées.
Générer un lien vers l’instant exact d’une vidéo
Pour YouTube, convertissez l’instant de départ en entier et ajoutez-le à l’URL source :
export function createYouTubeTimestampUrl(
videoUrl: string,
startSeconds: number,
): string {
const url = new URL(videoUrl)
url.searchParams.set('t', `${Math.floor(startSeconds)}s`)
return url.toString()
}Pour votre propre lecteur audio ou vidéo, transmettez l’horodatage au frontend et positionnez-vous directement :
player.currentTime = source.start
await player.play()C’est ce qui transforme une réponse générée en réponse vérifiable.
Cas d’usage d’un pipeline RAG vidéo
- Chercher dans une vidéothèque — interroger des milliers de vidéos par le sens plutôt que par le nom de fichier ou le titre.
- Interroger des supports de formation — cours internes, webinaires, vidéos d’onboarding et ateliers enregistrés.
- Construire un assistant de recherche — fouiller entretiens, podcasts, auditions et conférences, et renvoyer des citations précises.
- Créer une base de connaissances depuis une chaîne YouTube — ingérer un catalogue, stocker des chunks horodatés et rendre toute la chaîne interrogeable.
- Outiller la recherche éditoriale — retrouver une déclaration, un exemple, un nom ou un thème dans de vastes collections audio et vidéo.
- Ajouter la recherche IA à une plateforme média — indexer chaque média publié et exposer la recherche sémantique dans votre propre produit.
Ne reconstruisez pas la couche d’ingestion média
Un modèle de transcription seul ne résout pas l’ingestion des médias.
Si vous montez le pipeline vous-même, il faut aussi gérer :
- l’extraction depuis YouTube et les pages web ;
- l’analyse des flux de podcast ;
- le téléchargement de fichiers distants ;
- les redirections et les URL à durée de vie limitée ;
- l’extraction de la piste audio ;
- le stockage temporaire ;
- les formats de médias ;
- les reprises et les délais d’expiration ;
- les jobs de transcription ;
- la normalisation des horodatages ;
- les sources en double.
Techtuel expose une seule API de transcription vidéo pour toutes ces sources et renvoie une structure de transcription homogène.
Pour les sources publiques déjà traitées, le cache intégré évite en outre les retraitements inutiles.
Hébergement des données pour les applications RAG européennes
Techtuel est conçu et hébergé en France.
Le traitement des médias s’exécute sur une infrastructure européenne, les fichiers téléversés sont supprimés après traitement, et la transcription obtenue reste accessible via l’API jusqu’à ce que vous la supprimiez.
De quoi simplifier l’architecture des produits européens qui ne souhaitent pas faire transiter tout leur pipeline d’ingestion média par un hyperscaler américain. Le détail de cet hébergement, et les questions à poser à tout fournisseur, font l’objet d’un article dédié sur la transcription hébergée en Europe.
Il vous revient toujours de définir votre propre politique de conservation, vos contrôles d’accès et la base légale du traitement des contenus source.
De la transcription au RAG en production
Une architecture prête pour la production comporte en général cinq parties distinctes :
-
Ingestion Soumettre l’URL du média à
POST /v1/transcribe, et suivre le job quand la réponse est un202— médias longs ou rattrapage de catalogue. -
Normalisation Stocker la transcription, les segments, les métadonnées de la source et la langue.
-
Indexation Créer les chunks, les embeddings et les index de recherche.
-
Recherche Combiner recherche sémantique, recherche par mots-clés et filtres sur métadonnées.
-
Génération Demander au LLM de ne répondre qu’à partir des chunks retrouvés, avec des citations horodatées.
Garder ces étapes séparées permet de réindexer tout le catalogue sans retranscrire les médias d’origine.
Questions fréquentes
Peut-on faire du RAG directement depuis une URL YouTube ?
Oui. Envoyez l’URL YouTube à POST /v1/transcribe, récupérez la transcription structurée dans la réponse, créez vos chunks et stockez leurs embeddings dans votre base vectorielle.
L’API renvoie-t-elle des horodatages ?
Oui, des instants de départ. Chaque segment porte un start_seconds et son text. Il n’y a ni instant de fin ni horodatage au mot : un segment court jusqu’au début du suivant, à vous de reconstituer les intervalles. Les formats SRT et VTT sont également disponibles via ?format=srt ou ?format=vtt.
Peut-on traiter des vidéos sans sous-titres existants ?
Oui. Pour les sources YouTube, Techtuel exploite d’abord les sous-titres disponibles et bascule sur la transcription audio quand il n’y en a pas.
Quelle base vectorielle choisir ?
Techtuel est indépendant de la base vectorielle. pgvector, Qdrant, Weaviate, Pinecone, Elasticsearch ou tout autre moteur de recherche font l’affaire.
Faut-il vectoriser la transcription entière ?
Rarement. Les transcriptions longues gagnent à être découpées en chunks plus petits et cohérents, accompagnés des métadonnées de la source et des horodatages.
Peut-on réindexer sans retranscrire ?
Oui. Conservez la transcription complète et les segments d’origine dans votre propre base. Vous pourrez alors changer de stratégie de découpage ou d’embedding sans repasser par la transcription.
Comment savoir qu’un job est terminé ?
Le plus souvent, la question ne se pose pas : POST /v1/transcribe renvoie la transcription terminée directement dans sa réponse 200.
Quand vous obtenez un 202, ou que vous êtes passé par POST /v1/transcriptions, interrogez GET /v1/transcriptions/{id}. Seuls completed et failed sont terminaux — ce dernier porte un message dans error. Un job peut aussi apparaître en claimed : cela veut dire qu’un worker l’a pris en charge, continuez à l’interroger comme un processing. Il n’y a pas de rappel par webhook.
Passez à la pratique
Une seule API pour transformer vos URL audio et vidéo en texte horodaté, puis brancher le résultat sur votre pipeline RAG.