/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
- 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.
- 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.
- 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). - 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. - 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
- 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.
- Lisez la bande passante à côté de la fréquence d’échantillonnage.
telephone.wavn’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. - Deux explications pour un même mur comptent pour une seule constatation.
from-mp3-64k.wavdit « 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 :lossySourceresteunknownplutôt que de deviner, etupsampledFromHznomme 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. - Un raccord apparaît comme un raccord de bande passante.
spliced.waven 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é. - Stéréo en double, ce sont deux chiffres.
stereo-twice.wava deux canaux et unemonophonicProbabilityde 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. - L’écrêtage a deux colonnes.
hasClippingest vrai pour toute courte série d’échantillons au niveau crête du fichier lui-même.clippingRegionsne compte que les plages assez longues pour compter.eight-bit.wava la première et aucune région,clipped.waven a 22. Regardez les régions quand vous voulez l’entendre. - Bits utilisés.
effectiveBitDepthest 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 delossySource.
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: losslesssignifie 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.waven 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
- Practical
Comparer les fournisseurs d'agents vocaux et de synthèse vocale, et contrôler l'audio conversationnel
Trois appels d'API, dix chiffres, et jusqu'où faire confiance à chacun : un guide pour les équipes QA qui comparent des fournisseurs d'agents vocaux, de synthèse vocale ou d'isolation de la voix.
- Practical
Contrôler la qualité audio en CI : faire échouer un build quand un fichier enfreint vos règles
Un court script Python et un workflow GitHub qui mesurent chaque fichier audio avec l'API AudioLab, comparent les chiffres à des limites que vous fixez, et font échouer le build avec un message qui dit pourquoi.
- Intro
Ce que AudioLab ne fait pas
Les limites de l'analyse, énoncées simplement : il ne transcrit pas, ne prédit pas les erreurs de reconnaissance, ne note pas le naturel, n'identifie pas les locuteurs et ne mesure pas la latence, et ses indices de fond sont des suppositions.
- Intro
Changements et dépréciation : ce qui change, quand, et comment vous le savez
La promesse derrière /v1, ce qui compte comme un changement qui demande un préavis, comment nous l'annonçons, et ce qui est déprécié aujourd'hui.