Leçon 2 — Comprendre l'audio

Transcrire un appel et savoir qui parle

6 min de lecture

Transcrire un appel et savoir qui parle

Deux façons de transcrire

Modèle dédié gemini-3.5-transcribeModèle généraliste gemini-3.5-flash + consigne
Ce qu'on envoiel'audio seul + une configurationl'audio + « Transcris cet appel »
Ce qu'on reçoitdes segments structurés (audio_transcription) avec étiquette de locuteurdu texte libre, mis en forme à la discrétion du modèle
Diarisationoption diarization=True, étiquettes spk:0, spk:1demandée dans la consigne, format variable
Risqueerreurs d'écoute (chiffres, noms propres)idem, plus : reformulation, ajout d'un titre, de noms devinés
Usage idéalarchivage, recherche, sous-titres, analyse en avalré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

files/voix.py
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_codes est 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 lire part.audio_transcription.text et part.audio_transcription.speaker_label, ce que fait segments_from_parts dans voix.py.

La diarisation renvoie spk:0 et spk:1. Que sait-on avec certitude ?

Mesurer : le WER

Le WER (Word Error Rate, taux d'erreur sur les mots) compare une transcription à une référence exacte :

WER=S+D+IN\text{WER} = \frac{S + D + I}{N}

où SS = mots substitués, DD = mots supprimés, II = mots insérés et NN = 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.

files/voix.py
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 ?

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.0

Sur 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-transcribe et lire les segments audio_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
Suivant