Naar inhoud
Inloggen

/docs · General · Practical

Een dataset met gesprekken auditen: waar de audio echt vandaan komt

Meet een map met opnames van gesprekken, voice-agents of text-to-speech op een lossy bron, upsampling, plakplekken, mono vermomd als stereo, clipping en bitdiepte, 20 bestanden per keer, en weet wanneer de audio wordt geüpload en wanneer niet.

Bijgewerkt 12 okt 2026


Voor wie dit is

Je traint, fine-tunet of evalueert op gespreksaudio: opnames van gesprekken, logs van voice-agents, synthetische spraak, een corpus dat je hebt gescraped of gekocht. Voordat je de labels, de scores of een model erop vertrouwt, wil je weten wat de bestanden echt zijn. Een bestand met het label 16 kHz kan 8 kHz-telefoonaudio zijn die breder is gerekt. Een WAV kan een gedecodeerde MP3 zijn. Een stereobestand kan één kanaal twee keer zijn. Een opname kan geclipt zijn, en twee bronnen kunnen in één bestand aan elkaar zitten. Niets daarvan staat in de header.

Het is voor teams die zelf beslissen wat ze met een bevinding doen. Het beoordeelt geen labels of transcripties, leest de woorden niet, herkent geen sprekers en onderscheidt een synthetische stem niet van een echte. Zie wat AudioLab niet doet.

Wat je krijgt

  • Een script, audit_dataset.py, dat elk audiobestand in een map meet, 20 per keer, en één rij per bestand naar een CSV schrijft.
  • Een samenvatting van wat een blik waard is, met de bestandsnamen, en de bandbreedte per samplerate in de map.
  • Elke bevinding komt uit één analyse, de signaalanalyse (POST /v1/signallab), en elk getal dat het daar leest is gevalideerd: de codecmuur (lossySource), de oorspronkelijke samplerate achter een resampler (upsampledFromHz), naden tussen bronnen (bandwidthSplices), de kanalen en hoe gelijk ze zijn (channels, monophonicProbability), clipping (hasClipping, clippingRegions), de gebruikte bits (effectiveBitDepth) en de bandbreedte (bandwidthHz).

Voordat je begint

  1. Weet wanneer de audio wordt geüpload. Het script uploadt ze: elk bestand gaat naar de AudioLab-API, gehost in de EU, en alleen getallen komen terug. De werkbank is de andere weg: ze draait de engine in je browser en je audio wordt niet geüpload. Haar batch geeft je de tien getallen van de vergelijkingsgids over veel bestanden, met de hand, met de bandbreedte en de geclipte samples; ze noemt geen codecmuur, geen resampling, geen naad en geen kanaalindeling. Als je opnames je eigen systemen niet mogen verlaten, gebruik dan de werkbank en accepteer dat ze minder dekt.
  2. Weet wat de opnames bevatten. Opnames van gesprekken bevatten meestal persoonsgegevens. Ze naar de API sturen maakt ons jouw verwerker: lees de verwerkersovereenkomst en /privacy voordat je het script op echte opnames draait, en begin met bestanden die je vrij mag delen.
  3. Haal een sleutel. Hij is gratis en vraagt geen kaart: log in op /account en genereer er een. Het script heeft hem nodig, want het vraagt eerst wat je abonnement nog heeft (GET /v1/usage, dat gratis is en niet als aanroep telt).
  4. Weet wat het kost. Elk bestand is één aanroep, en zijn grootte telt mee voor je data van de maand, dus op de gratis tier (1.000 aanroepen per maand, 2 GB per maand en 60 verzoeken per minuut) audit je een steekproef, geen corpus van tienduizend. Een lokaal bestand vraagt twee verzoeken: één om het te uploaden en één om het te meten. Het script houdt zich aan de verzoeken per minuut van je abonnement, dus 300 bestanden bij 60 per minuut duren minstens tien minuten. Begin met --sample 300: een willekeurige steekproef, elke keer dezelfde, is genoeg om te zien of er een probleem is.
  5. Python 3.9 of nieuwer, en het pakket:
pip install audiolabtools

Het 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))

Het somt de audiobestanden in de map op, neemt de steekproef als je erom vraagt, vraagt wat je abonnement nog heeft en weigert een run die niet past, en stuurt de bestanden dan naar POST /v1/batch met endpoint="signallab", 20 per keer, elk eerst geüpload. Na elke batch schrijft het de CSV. Als de API midden in een batch stopt (een volle minuut, of een limiet van je abonnement), zegt het script dat en stopt het; draai het opnieuw en de bestanden die al in de CSV staan, worden overgeslagen.

Een run op zeven opnames

Om te laten zien hoe de bevindingen eruitzien, hebben we zeven opnames gebouwd uit een van onze eigen stemvoorbeelden, elk met één bekend gebrek, en het script erop gedraaid. De opnames zijn gemeten door de engine, en het script is gedraaid tegen een stand-in voor de API met die antwoorden, dus wat volgt is wat het script print.

  • clean.wav: de stem en een rustige ruisvloer, als 16-bit WAV op 44,1 kHz.
  • telephone.wav: teruggebracht naar 8 kHz en weer omhoog naar 16 kHz.
  • from-mp3-64k.wav: gecodeerd als MP3 van 64 kbps en gedecodeerd naar WAV.
  • clipped.wav: 24 dB luider dan het formaat toestaat.
  • stereo-twice.wav: dezelfde stem in beide kanalen van een stereobestand.
  • spliced.wav: acht seconden zoals het is, daarna vier seconden van dezelfde stem op telefoonbandbreedte.
  • eight-bit.wav: opgeslagen als 8-bit samples.
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

De CSV heeft één rij per bestand met de bevindingen en de getallen erachter. De bevindingenkolom van deze zeven:

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     -

Zo lees je het

  1. Een bevinding is een reden om te luisteren, geen oordeel. Open de CSV, sorteer op de kolom waar je om geeft en luister naar de bestanden bovenaan.
  2. Lees de bandbreedte naast de samplerate. telephone.wav heeft helemaal geen bevinding, en het is degene die het meest telt: zijn bandbreedte is 4.297 Hz in een bestand op 16.000 Hz, dus er zat nooit iets boven ongeveer 4 kHz in. De engine noemt een oorspronkelijke rate (upsampledFromHz) alleen als de roll-off begint waar een standaardrate hem zou leggen, en zegt anders niets, dus hier is de samenvatting van de bandbreedte per samplerate wat het laat zien. Zoek een samplerate met een grote spreiding, of een bestand ver onder de andere.
  3. Twee verklaringen voor één muur zijn één bevinding. from-mp3-64k.wav zegt “upsampled from 32.000 Hz” en “source unclear”. Zijn bandbreedte is 16.516 Hz in een bestand op 44,1 kHz. Een MP3 met lage bitrate en een geresamplede 32 kHz-bron laten een vergelijkbare muur achter: lossySource blijft unknown in plaats van te gokken, en upsampledFromHz noemt een 32 kHz-bron. De oorzaak was hier een MP3, dus die tweede regel is een verkeerde verklaring van een juiste waarneming, en de pagina van de maat zegt dat een codec met een zeer lage bitrate een resampler kan nabootsen. Voor jouw doel zeggen beide regels hetzelfde: niets boven ongeveer 16,5 kHz, dus dit bestand draagt niet wat zijn samplerate belooft.
  4. Een naad toont zich als een bandwidth splice. spliced.wav heeft er een: de bandbreedte sprong met meer dan 3 kHz en bleef daar minstens twee seconden, waar het deel in de telefoonband begint. In muziek kan een telling ook een wisseling in het arrangement zijn. In een gesprek is het vaker een andere bron, een andere codec of een ingeplakt fragment.
  5. Stereo twee keer is twee getallen. stereo-twice.wav heeft twee kanalen en een monophonicProbability van 1. De naam misleidt: het is de correlatie tussen links en rechts, nooit onder 0, en dicht bij 1 betekent mono vermomd als stereo. Het script noemt een stereobestand met 0,99 of meer “same both sides”; die limiet is van ons. Als je opnames elke spreker op een eigen kanaal zetten, betekent een waarde dicht bij 1 dat ze zijn samengemixt.
  6. Clipping heeft twee kolommen. hasClipping is true voor elke korte reeks samples op de eigen piek van het bestand. clippingRegions telt alleen de stukken die lang genoeg zijn om te tellen. eight-bit.wav heeft de eerste en geen regio’s, clipped.wav heeft er 22. Kijk naar de regio’s als je het wilt horen.
  7. Gebruikte bits. effectiveBitDepth is het aantal bits dat de samples echt gebruiken. Een bestand dat als 16 bits is opgeslagen en 8 leest, had zijn resolutie al verlaagd voordat het werd opgeslagen. Een lossy bestand dat on the fly is gedecodeerd, leest 24, en daarom vindt dit getal geen lossy bronnen: daar is lossySource voor.

Wat het niet kan

  • Het ziet niet elke lossy bron. De engine zoekt de muur die een encoder achterlaat. Onze eigen stemvoorbeelden zijn MP3-bestanden en de engine leest ze als lossless: hij vond er geen muur in. lossySource: lossless betekent dat er geen muur is gevonden, niet dat het bestand nooit lossy is geweest.
  • Het noemt niet altijd de oorspronkelijke rate van een geresampled bestand. telephone.wav is het voorbeeld. De pagina van de maat somt de gevallen op die hij bewust met rust laat.
  • Het bewijst niet dat twee bestanden een bron delen: er is geen vingerafdruk. Een naad tussen twee opnames van dezelfde soort laat geen spoor achter in de band.
  • Het zegt niet of een stem synthetisch is, herkent geen spreker, controleert geen label en leest geen transcript.
  • Een batch is één aanroep per bestand op volgorde, dus een groot corpus is traag en kost aanroepen. Neem eerst een steekproef.

Privacy en limieten

  • Je audio wordt niet bewaard. Ze wordt in het geheugen geanalyseerd in Frankfurt. Een gecomprimeerd bestand (MP3, FLAC, M4A) gaat via een tijdelijk bestand dat direct na het decoderen wordt verwijderd. Een bestand dat je via de signed-upload-route uploadt, wordt binnen het uur verwijderd. Wij bewaren de getallen en, als je dat toestaat, de bestandsnaam. Zie /privacy.
  • Limieten. Gratis: 1.000 aanroepen en 2 GB per maand, 60 verzoeken per minuut. Plus: 10.000 aanroepen, 20 GB, 120 per minuut. Een batch neemt maximaal 20 bestanden, een bestand mag 150 MB zijn, en elk bestand in een batch is één aanroep. Elk geaccepteerd verzoek telt mee, ook als het daarna faalt. Een bestand dat niet kan worden gemeten, is een rij met zijn fout in de CSV, en de andere bestanden worden nog steeds gemeten.
  • Waar je staat. AudioLab().usage() geeft het abonnement en de deze maand gebruikte aanroepen en data terug, tegen de limieten. Het script vraagt het voordat het iets verstuurt. Het lezen is gratis en telt niet als aanroep.

Volgende in General docs

Een respons lezen: wat elk veld betekent en hoe ver je het kunt vertrouwen →

Een echt antwoord van de API, veld voor veld: wat het zegt, de status en de marge, en wat je er wel en niet uit mag afleiden.

More in General docs