Aller au contenu
Se connecter

/docs · General · Practical

Auditer un jeu de conversations : d'où vient vraiment l'audio

Mesurez un dossier d'enregistrements d'appels, d'agents vocaux ou de synthèse vocale pour y repérer une source avec pertes, un suréchantillonnage, des raccords, du mono déguisé, de l'écrêtage et la profondeur de bits, 20 fichiers à la fois, et sachez quand l'audio est téléversé et quand il ne l'est pas.

Mis à jour 12 oct. 2026


À qui s’adresse ce guide

Vous entraînez, affinez ou évaluez des modèles sur de l’audio conversationnel : enregistrements d’appels, journaux d’agents vocaux, parole synthétique, un corpus que vous avez collecté ou acheté. Avant de faire confiance aux étiquettes, aux scores ou à un modèle construit dessus, vous voulez savoir ce que sont vraiment les fichiers. Un fichier étiqueté 16 kHz peut être de l’audio téléphonique à 8 kHz étiré vers le haut. Un WAV peut être un MP3 décodé. Un fichier stéréo peut être un seul canal en double. Une prise peut être écrêtée, et deux sources peuvent être collées dans un même fichier. Rien de tout cela n’apparaît dans l’en-tête.

Il s’adresse aux équipes qui décident elles-mêmes quoi faire d’une constatation. Il ne juge ni les étiquettes ni les transcriptions, ne lit pas les mots, n’identifie pas les locuteurs et ne distingue pas une voix synthétique d’une voix réelle. Voir ce que AudioLab ne fait pas.

Ce que vous obtenez

  • Un script, audit_dataset.py, qui mesure chaque fichier audio d’un dossier, 20 à la fois, et écrit une ligne par fichier dans un CSV.
  • Une synthèse de ce qui mérite un coup d’œil, avec les noms de fichiers, et la bande passante pour chaque fréquence d’échantillonnage présente dans le dossier.
  • Chaque constatation vient d’une seule analyse, l’analyse du signal (POST /v1/signallab), et chaque chiffre qu’il y lit est validé : le mur du codec (lossySource), la fréquence d’échantillonnage d’origine derrière un rééchantillonneur (upsampledFromHz), les raccords entre sources (bandwidthSplices), les canaux et leur ressemblance (channels, monophonicProbability), l’écrêtage (hasClipping, clippingRegions), les bits utilisés (effectiveBitDepth) et la bande passante (bandwidthHz).

Avant de commencer

  1. Sachez quand l’audio est téléversé. Le script le téléverse : chaque fichier va à l’API AudioLab, hébergée dans l’UE, et seuls des chiffres reviennent. L’espace de travail est l’autre voie : il exécute le moteur dans votre navigateur et ne téléverse rien. Son traitement par lots vous donne les dix chiffres du guide de comparaison sur de nombreux fichiers, à la main, avec la bande passante et les échantillons écrêtés ; il ne nomme ni mur de codec, ni rééchantillonnage, ni raccord, ni disposition des canaux. Si vos enregistrements ne doivent pas quitter vos propres systèmes, utilisez l’espace de travail et acceptez qu’il en couvre moins.
  2. Sachez ce que contiennent les enregistrements. Les enregistrements d’appels contiennent généralement des données personnelles. Les envoyer à l’API fait de nous votre sous-traitant : lisez l’avenant de traitement des données et /privacy avant de lancer le script sur de vrais enregistrements, et commencez par des fichiers que vous avez le droit de partager.
  3. Obtenez une clé. Elle est gratuite et ne demande pas de carte : connectez-vous sur /account et générez-en une. Le script en a besoin, car il demande d’abord ce qu’il reste à votre offre (GET /v1/usage, qui est gratuit et ne compte pas comme un appel).
  4. Sachez ce que cela coûte. Chaque fichier est un appel, et sa taille est décomptée de vos données du mois ; avec le palier gratuit (1 000 appels par mois, 2 Go par mois et 60 requêtes par minute), vous auditez donc un échantillon, pas un corpus de dix mille fichiers. Un fichier local demande deux requêtes : une pour le téléverser et une pour le mesurer. Le script se cadence sur les requêtes par minute de votre offre : 300 fichiers à 60 par minute prennent donc au moins dix minutes. Commencez par --sample 300 : un échantillon aléatoire, toujours le même, suffit pour voir s’il y a un problème.
  5. Python 3.9 ou plus récent, et le paquet :
pip install audiolabtools

Le script

"""Audit a folder of audio files for where the audio really comes from.

One row per file in a CSV, and a summary of what is worth a look.

    python audit_dataset.py audio/ audit.csv               every file in audio/, 20 at a time
    python audit_dataset.py audio/ audit.csv --sample 300  a random sample of 300, the same
                                                           300 every time: the cheap first look

https://audiolab.tools/docs/start/audit-a-conversation-dataset
Every file is uploaded to the AudioLab API (https://audiolab.tools/privacy) and counts as one
call, so this needs a key (AUDIOLAB_KEY). A file that is already in the CSV is skipped, so a
run that stopped can be started again.
Exit code: 0 when every file was measured, 2 when something could not be (the CSV says what).
"""

from __future__ import annotations

import argparse
import csv
import random
import statistics
import sys
import time
from pathlib import Path

from audiolabtools import AudioLab, AudioLabError

AUDIO = {".wav", ".mp3", ".flac", ".m4a", ".ogg", ".opus", ".aac", ".aif", ".aiff"}
FIELDS = [
    "sampleRate", "channels", "bandwidthHz", "upsampledFromHz", "lossySource",
    "bandwidthSplices", "effectiveBitDepth", "monophonicProbability",
    "hasClipping", "clippingRegions",
]
COLUMNS = ["file", "findings", *FIELDS, "error"]
BATCH = 20  # the most the API takes in one call
# On a stereo file a monophonicProbability at or above this counts as the same signal on both
# sides. That is our choice: the number says only that near 1 means mono in disguise.
SAME_BOTH_SIDES = 0.99
KINDS = [
    "upsampled", "lossy source", "source unclear", "bandwidth splices",
    "stereo, same both sides", "clipping", "fewer than 16 bits",
]


def findings(d):
    """What is worth a look in one file, as 'kind (detail)'.

    Only what the engine itself says. The limits that are ours: SAME_BOTH_SIDES and 16 bits.
    """
    found = []
    if d.get("upsampledFromHz") is not None:
        found.append(f"upsampled (from {d['upsampledFromHz']:g} Hz)")
    if str(d.get("lossySource")).startswith("lossy"):
        found.append(f"lossy source ({d['lossySource'][6:]})")
    elif d.get("lossySource") == "unknown":
        found.append("source unclear")
    if d.get("bandwidthSplices"):
        found.append(f"bandwidth splices ({d['bandwidthSplices']})")
    if d.get("channels") == 2 and (d.get("monophonicProbability") or 0) >= SAME_BOTH_SIDES:
        found.append("stereo, same both sides")
    if d.get("hasClipping"):
        regions = d.get("clippingRegions") or 0
        found.append(
            f"clipping ({regions} region{'' if regions == 1 else 's'})"
            if regions else "clipping (a short run at the peak)"
        )
    if d.get("effectiveBitDepth") is not None and d["effectiveBitDepth"] < 16:
        found.append(f"fewer than 16 bits ({d['effectiveBitDepth']})")
    return found


def write_csv(path, rows):
    with open(path, "w", newline="", encoding="utf-8") as f:
        writer = csv.DictWriter(f, fieldnames=COLUMNS)
        writer.writeheader()
        writer.writerows(rows.values())


def summary(rows, out):
    measured = [r for r in rows.values() if not r["error"]]
    plural = "" if len(measured) == 1 else "s"
    out(f"\n{len(measured)} file{plural} measured, {len(rows) - len(measured)} failed\n")
    out(f"{'what':25}{'files':>5}")
    for kind in KINDS:
        hit = [
            r["file"] for r in measured
            if any(x == kind or x.startswith(kind + " (") for x in r["findings"].split("; "))
        ]
        names = ", ".join(hit[:5]) + (f" and {len(hit) - 5} more" if len(hit) > 5 else "")
        out(f"{kind:25}{len(hit):>5}   {names}".rstrip())
    out("\nbandwidth by sample rate")
    for rate in sorted({int(float(r["sampleRate"])) for r in measured}):
        widths = sorted(float(r["bandwidthHz"]) for r in measured
                        if int(float(r["sampleRate"])) == rate)
        if len(widths) == 1:
            out(f"  {rate} Hz    1 file   {widths[0]:.0f} Hz")
        else:
            median = statistics.median(widths)
            out(f"  {rate} Hz  {len(widths):>3} files  {widths[0]:.0f} to {widths[-1]:.0f} Hz, "
                f"median {median:.0f}")


def main(argv, client=None, out=print, wait=time.sleep):
    ask = argparse.ArgumentParser(description="Audit a folder of audio files.")
    ask.add_argument("folder")
    ask.add_argument("csv")
    ask.add_argument("--sample", type=int, help="measure a random sample of this many files")
    args = ask.parse_args(argv[1:])
    folder, target = Path(args.folder), Path(args.csv)

    files = sorted(p for p in folder.rglob("*") if p.suffix.lower() in AUDIO)
    if args.sample and args.sample < len(files):
        files = sorted(random.Random(1).sample(files, args.sample))
    rows = {}
    if target.exists():
        with open(target, newline="", encoding="utf-8") as f:
            rows = {r["file"]: r for r in csv.DictReader(f)}

    def name(p):
        return p.relative_to(folder).as_posix()

    todo = [p for p in files if name(p) not in rows or rows[name(p)]["error"]]

    client = client or AudioLab()  # reads AUDIOLAB_KEY
    try:
        usage = client.usage()  # free, and it says what this run may cost before it costs it
        size = sum(p.stat().st_size for p in todo)
        calls_left, bytes_left = usage["calls"]["remaining"], usage["data"]["remainingBytes"]
        left = "no limit" if calls_left is None else f"{calls_left} calls"
        out(f"{len(todo)} files to measure ({size / 1e6:.0f} MB). Plan {usage['plan']}: "
            f"{left} left this month, {usage['requestsPerMinute']} requests a minute.")
        if (calls_left is not None and len(todo) > calls_left) or (
            bytes_left is not None and size > bytes_left
        ):
            out("That is more than your plan has left this month. "
                "Start with --sample, or wait for the new month.")
            return 2

        result = 0
        for i in range(0, len(todo), BATCH):
            chunk = todo[i : i + BATCH]
            started = time.monotonic()
            answer = client.batch([str(p) for p in chunk], endpoint="signallab", round=True)
            for p, r in zip(chunk, answer["results"]):
                d = r.get("data") or {}
                row = {
                    "file": name(p),
                    "findings": "; ".join(findings(d)) if r["ok"] else "",
                    "error": "" if r["ok"] else r.get("error", "failed"),
                }
                row.update({k: d.get(k, "") for k in FIELDS})
                rows[row["file"]] = row
                if not r["ok"]:
                    result = 2
            write_csv(target, rows)
            if len(answer["results"]) < len(chunk):
                # the API stopped inside the batch: a full minute, or a limit of your plan
                why = (answer["results"] or [{}])[-1].get("error", "no reason given")
                done = len(answer["results"])
                out(f"The API stopped after {done} of {len(chunk)} files: {why}")
                out("Run it again: the files already in the CSV are skipped.")
                return 2
            # A local file takes two requests: one to mint its signed upload, one for the
            # analysis. Stay under the requests a minute of the plan.
            pace = 2 * len(chunk) * 60 / usage["requestsPerMinute"]
            wait(max(0, pace - (time.monotonic() - started)))
    except AudioLabError as e:
        out(f"Stopped: {e}")
        return 2
    write_csv(target, rows)
    summary(rows, out)
    return result


if __name__ == "__main__":
    sys.exit(main(sys.argv))

Il liste les fichiers audio du dossier, prend l’échantillon si vous en demandez un, demande ce qu’il reste à votre offre et refuse une exécution qui n’y tient pas, puis envoie les fichiers à POST /v1/batch avec endpoint="signallab", 20 à la fois, chacun téléversé d’abord. Après chaque lot, il écrit le CSV. Si l’API s’arrête au milieu d’un lot (une minute pleine, ou une limite de votre offre), le script le dit et se termine ; relancez-le et les fichiers déjà présents dans le CSV sont ignorés.

Une exécution sur sept enregistrements

Pour montrer à quoi ressemblent les constatations, nous avons construit sept enregistrements à partir d’un de nos propres échantillons de voix, chacun avec un défaut connu, et lancé le script dessus. Les enregistrements ont été mesurés par le moteur, et le script a été exécuté contre une doublure de l’API avec ces réponses : ce qui suit est donc ce que le script affiche.

  • clean.wav : la voix et un faible bruit de fond, en WAV 16 bits à 44,1 kHz.
  • telephone.wav : ramené à 8 kHz puis remonté à 16 kHz.
  • from-mp3-64k.wav : encodé en MP3 à 64 kbit/s puis décodé en WAV.
  • clipped.wav : 24 dB plus fort que ce que le format permet.
  • stereo-twice.wav : la même voix dans les deux canaux d’un fichier stéréo.
  • spliced.wav : huit secondes telles quelles, puis quatre secondes de la même voix à la largeur de bande du téléphone.
  • eight-bit.wav : stocké en échantillons de 8 bits.
7 files to measure (14 MB). Plan plus: 9988 calls left this month, 120 requests a minute.

7 files measured, 0 failed

what                     files
upsampled                    1   from-mp3-64k.wav
lossy source                 0
source unclear               2   from-mp3-64k.wav, spliced.wav
bandwidth splices            1   spliced.wav
stereo, same both sides      1   stereo-twice.wav
clipping                     2   clipped.wav, eight-bit.wav
fewer than 16 bits           1   eight-bit.wav

bandwidth by sample rate
  16000 Hz    1 file   4297 Hz
  44100 Hz    6 files  16516 to 22028 Hz, median 22028

Le CSV a une ligne par fichier, avec les constatations et les chiffres qui les fondent. La colonne des constatations pour ces sept fichiers :

clean.wav         -
clipped.wav       clipping (22 regions)
eight-bit.wav     clipping (a short run at the peak); fewer than 16 bits (8)
from-mp3-64k.wav  upsampled (from 32000 Hz); source unclear
spliced.wav       source unclear; bandwidth splices (1)
stereo-twice.wav  stereo, same both sides
telephone.wav     -

Comment le lire

  1. Une constatation est une raison d’écouter, pas un verdict. Ouvrez le CSV, triez sur la colonne qui vous intéresse et écoutez les fichiers du haut.
  2. Lisez la bande passante à côté de la fréquence d’échantillonnage. telephone.wav n’a aucune constatation, et c’est celui qui compte le plus : sa bande passante est de 4 297 Hz dans un fichier à 16 000 Hz, donc rien au-dessus d’environ 4 kHz n’y a jamais figuré. Le moteur ne nomme une fréquence d’origine (upsampledFromHz) que lorsque l’atténuation commence là où une fréquence standard la placerait, et sinon il ne dit rien ; ici, c’est donc la synthèse de la bande passante par fréquence d’échantillonnage qui le montre. Cherchez une fréquence d’échantillonnage avec une grande dispersion, ou un fichier très en dessous des autres.
  3. Deux explications pour un même mur comptent pour une seule constatation. from-mp3-64k.wav dit « upsampled from 32 000 Hz » et « source unclear ». Sa bande passante est de 16 516 Hz dans un fichier à 44,1 kHz. Un MP3 à faible débit et une source à 32 kHz rééchantillonnée laissent un mur semblable : lossySource reste unknown plutôt que de deviner, et upsampledFromHz nomme une source à 32 kHz. La cause ici était un MP3 : cette seconde ligne est donc une mauvaise explication d’une bonne observation, et la page de la mesure précise qu’un codec à très faible débit peut imiter un rééchantillonneur. Pour votre usage, les deux lignes disent la même chose : rien au-dessus d’environ 16,5 kHz, donc ce fichier ne porte pas ce que sa fréquence d’échantillonnage promet.
  4. Un raccord apparaît comme un raccord de bande passante. spliced.wav en a un : la bande passante a sauté de plus de 3 kHz et y est restée au moins deux secondes, là où commence la partie en bande téléphonique. En musique, un compte peut aussi être un changement d’arrangement. Dans une conversation, c’est plus souvent une autre source, un autre codec, ou un extrait collé.
  5. Stéréo en double, ce sont deux chiffres. stereo-twice.wav a deux canaux et une monophonicProbability de 1. Le nom est trompeur : c’est la corrélation entre gauche et droite, jamais inférieure à 0, et proche de 1 signifie du mono déguisé. Le script appelle « same both sides » un fichier stéréo à 0,99 ou plus ; cette limite est la nôtre. Si vos enregistrements placent chaque locuteur sur son propre canal, une valeur proche de 1 signifie qu’ils ont été mélangés en un seul.
  6. L’écrêtage a deux colonnes. hasClipping est vrai pour toute courte série d’échantillons au niveau crête du fichier lui-même. clippingRegions ne compte que les plages assez longues pour compter. eight-bit.wav a la première et aucune région, clipped.wav en a 22. Regardez les régions quand vous voulez l’entendre.
  7. Bits utilisés. effectiveBitDepth est le nombre de bits que les échantillons utilisent réellement. Un fichier stocké en 16 bits qui indique 8 a vu sa résolution réduite avant d’être stocké. Un fichier avec pertes décodé à la volée indique 24, c’est pourquoi ce chiffre ne trouve pas les sources avec pertes : c’est le rôle de lossySource.

Ce qu’il ne peut pas faire

  • Il ne voit pas toutes les sources avec pertes. Le moteur cherche le mur qu’un encodeur laisse. Nos propres échantillons de voix sont des fichiers MP3 et le moteur les lit comme sans perte : il n’y a trouvé aucun mur. lossySource: lossless signifie qu’aucun mur n’a été trouvé, pas que le fichier n’a jamais subi de pertes.
  • Il ne nomme pas toujours la fréquence d’origine d’un fichier rééchantillonné. telephone.wav en est l’exemple. La page de la mesure liste les cas qu’il laisse volontairement de côté.
  • Il ne prouve pas que deux fichiers partagent une source : il n’y a pas d’empreinte. Un raccord entre deux enregistrements du même genre ne laisse aucune trace dans la bande.
  • Il ne dit pas si une voix est synthétique, n’identifie pas un locuteur, ne vérifie pas une étiquette et ne lit pas une transcription.
  • Un lot, c’est un appel par fichier en séquence : un gros corpus est donc lent et coûte des appels. Échantillonnez d’abord.

Confidentialité et limites

  • Votre audio n’est pas conservé. Il est analysé en mémoire à Francfort. Un fichier compressé (MP3, FLAC, M4A) passe par un fichier temporaire supprimé juste après le décodage. Un fichier que vous téléversez par la voie d’envoi signé est supprimé dans l’heure. Nous conservons les chiffres et, si vous nous y autorisez, le nom du fichier. Voir /privacy.
  • Limites. Gratuit : 1 000 appels et 2 Go par mois, 60 requêtes par minute. Plus : 10 000 appels, 20 Go, 120 par minute. Un lot accepte jusqu’à 20 fichiers, un fichier peut peser 150 Mo, et chaque fichier d’un lot est un appel. Chaque requête acceptée compte, y compris celle qui échoue ensuite. Un fichier qui ne peut pas être mesuré devient une ligne avec son erreur dans le CSV, et les autres fichiers sont tout de même mesurés.
  • Où vous en êtes. AudioLab().usage() renvoie l’offre ainsi que les appels et les données utilisés ce mois-ci par rapport aux limites. Le script le demande avant d’envoyer quoi que ce soit. La lecture est gratuite et ne compte pas comme un appel.

Ensuite dans General docs

Lire une réponse : ce que signifie chaque champ et jusqu'où lui faire confiance →

Une vraie réponse de l'API, champ par champ : ce qu'elle dit, son statut et sa marge, et ce que vous pouvez ou ne pouvez pas en conclure.

More in General docs