Transcrire un appel et savoir qui parle
Deux façons de transcrire
Modèle dédié gemini-3.5-transcribe | Modèle généraliste gemini-3.5-flash + consigne | |
|---|---|---|
| Ce qu'on envoie | l'audio seul + une configuration | l'audio + « Transcris cet appel » |
| Ce qu'on reçoit | des segments structurés (audio_transcription) avec étiquette de locuteur | du texte libre, mis en forme à la discrétion du modèle |
| Diarisation | option diarization=True, étiquettes spk:0, spk:1 | demandée dans la consigne, format variable |
| Risque | erreurs d'écoute (chiffres, noms propres) | idem, plus : reformulation, ajout d'un titre, de noms devinés |
| Usage idéal | archivage, recherche, sous-titres, analyse en aval | résumé direct quand le verbatim importe peu |
La diarisation (de l'anglais diarization, « tenir le journal ») répond à la question « qui parle quand ? ». Elle ne donne pas de noms : elle attribue la même étiquette aux passages d'une même voix. C'est à vous (ou au LLM en aval) de dire que spk:0 est l'agent.
flowchart LR A["appel.wav"] --> C["AudioTranscriptionConfig : diarization, language_codes"] C --> M["gemini-3.5-transcribe"] M --> P["Parts audio_transcription"] P --> F["Fusion des segments consécutifs"] F --> S["spk:0 Bonjour... / spk:1 Ma commande..."]
Le code
def transcribe(c, path: str, language: str = "fr-FR") -> list[dict]:
"""Transcription + diarisation (qui parle quand). Renvoie [{"speaker": "spk:0", "text": ...}]."""
from google.genai import types
audio = types.Part.from_bytes(data=open(path, "rb").read(), mime_type="audio/wav")
cfg = types.GenerateContentConfig(audio_transcription_config=types.AudioTranscriptionConfig(
diarization=True, language_codes=[language]))
r = with_retry(lambda: c.models.generate_content(model=STT_MODEL, contents=[audio], config=cfg))
return segments_from_parts(r.candidates[0].content.parts)Trois remarques :
- L'audio part en ligne (
Part.from_bytes) : c'est possible tant que la requête complète reste sous 20 Mo. Au-delà, téléversez-le avec l'API Files (client.files.upload). language_codesest un indice, pas une obligation : sans lui, la langue est détectée automatiquement.- La réponse n'a pas de
response.text: le SDK prévient « non-text parts in the response ». Il faut lirepart.audio_transcription.textetpart.audio_transcription.speaker_label, ce que faitsegments_from_partsdans voix.py.
La diarisation renvoie spk:0 et spk:1. Que sait-on avec certitude ?
La diarisation regroupe les passages par voix, sans identité. Déduire le rôle (agent ou client) est une étape séparée, ici confiée au LLM qui rédige le compte rendu (champ role_agent).
Mesurer : le WER
Le WER (Word Error Rate, taux d'erreur sur les mots) compare une transcription à une référence exacte :
où = mots substitués, = mots supprimés, = mots insérés et = nombre de mots de la référence. C'est une distance d'édition, calculée mot par mot. Notre fonction wer() normalise d'abord la casse et la ponctuation, sinon « Bonjour. » et « bonjour » compteraient comme une erreur.
def wer(reference: str, hypothesis: str) -> float:
norm = lambda s: "".join(ch.lower() if ch.isalnum() or ch in " '" else " " for ch in s).split()
r, h = norm(reference), norm(hypothesis)
d = list(range(len(h) + 1)) # distance d'édition sur les mots, une ligne à la fois
for i, rw in enumerate(r, 1):
prev, d[0] = d[0], i
for j, hw in enumerate(h, 1):
prev, d[j] = d[j], min(d[j] + 1, d[j - 1] + 1, prev + (rw != hw))
return d[len(h)] / max(len(r), 1)Résultat obtenu
Sortie réelle du 2026-10-15 sur un dialogue de 3 répliques synthétisé avec deux voix (environ 11 s, 272 tokens audio comptés par l'API) :
# gemini-3.5-transcribe, diarization=True, language_codes=["fr-FR"] (1,65 s)
{'text': "Bonjour, service client atelier, Léa à l'appareil.", 'speaker_label': 'spk:0'}
{'text': "Bonjour, ma commande n'est toujours pas arrivée, ça fait 10 jours.", 'speaker_label': 'spk:1'}
{'text': 'Je comprends, je regarde tout de suite.', 'speaker_label': 'spk:0'}
# gemini-3.5-flash + « Transcris cet audio. Indique le locuteur. » (3,56 s)
Voici la transcription de l'audio : **Locutrice 1 (Léa) :** Bonjour, service client Atelier,
Léa à l'appareil. **Locuteur 2 (Client) :** Bonjour, ma commande n'est toujours pas arrivée...Sur l'appel complet du projet final (7 répliques, 41 s), le WER mesuré a été de 2,6 % sur une exécution et de 3,4 % sur une autre ; lors de cette dernière, « commande 4521 » a été transcrit « commande 451 ». Les autres « erreurs » étaient surtout « dix » → « 10 » et « Atelier » → « atelier ».
Sur 38 mots de référence, la transcription a 1 substitution, 0 suppression et 1 insertion. Quel est le WER ?
WER = (S + D + I) / N = (1 + 0 + 1) / 38 ≈ 0,053, soit environ 5,3 %.
Exercice : un taux d'erreur sur les entités
Écrivez une fonction entity_recall(reference, hypothesis) qui extrait tous les nombres de 3 chiffres ou plus de la référence et renvoie la proportion retrouvée à l'identique dans la transcription.
Voir la solution
import re
def entity_recall(reference: str, hypothesis: str) -> float:
expected = re.findall(r"\d{3,}", reference)
found = set(re.findall(r"\d{3,}", hypothesis))
return sum(e in found for e in expected) / max(len(expected), 1)
assert entity_recall("commande 4521", "la commande 4521 et") == 1.0
assert entity_recall("commande 4521", "la commande 451") == 0.0Sur un vrai jeu d'appels, suivez ce taux à part du WER : c'est lui qui prédit les mauvaises actions.
Je sais faire
- Transcrire un fichier avec
gemini-3.5-transcribeet lire les segmentsaudio_transcription - Activer la diarisation et expliquer ce qu'elle garantit (et ce qu'elle ne garantit pas)
- Calculer un WER et dire pourquoi il ne suffit pas
- Choisir entre modèle dédié et modèle généraliste pour transcrire