/docs · General · Practical
Audioqualität in der CI prüfen: einen Build scheitern lassen, wenn eine Datei deine Regeln bricht
Ein kurzes Python-Skript und ein GitHub-Workflow, die jede Audiodatei mit der AudioLab-API messen, die Zahlen mit deinen Limits vergleichen und den Build mit einer Meldung scheitern lassen, die sagt, warum.
Aktualisiert 12.10.2026
Für wen das ist
Du lieferst Audio aus einer Pipeline aus: Text-to-Speech-Prompts, Aufnahmen von Voice Agents, Podcast-Folgen, Sound für Spiele oder Apps. Ein Build soll stoppen, wenn eine Datei zu leise ist, clippt, bandbegrenzt oder verrauscht ist, bevor ein Hörer es hört.
Gedacht ist es für Teams, die ihre eigenen Limits setzen. AudioLab misst die Datei; ob eine Zahl besteht, ist deine Regel. Es beurteilt nicht, wie natürlich eine Stimme klingt, und es liest die Wörter nicht. Siehe was AudioLab nicht tut.
Was du bekommst
- Ein Skript,
audio_gate.py, das jede Datei misst, die du ihm gibst, deine Limits prüft und mit einem Exit-Code endet, den deine CI versteht. Seine einzige Abhängigkeit ist das Paketaudiolabtools. - Eine Limits-Datei,
gate.json, mit einem Eintrag pro Zahl. - Einen Workflow,
audio-gate.yml, der das Skript bei Pull Requests ausführt und nur die Audiodateien misst, die der Pull Request hinzufügt oder ändert. - Jede fehlgeschlagene Zeile nennt die Zahl, ihren Wert und das Limit und sagt, wie weit du der Zahl trauen kannst: ihren Status und ihre Marge. Wer einen roten Build liest, weiß, was er vor sich hat. Siehe wie du eine Antwort liest.
Bevor du anfängst
- Hol dir einen Schlüssel. Er ist kostenlos und braucht keine Karte: Melde dich auf /account an und erzeuge einen. In GitHub legst du ihn als Repository-Secret namens
AUDIOLAB_KEYab. Committe ihn nie. - Wisse, dass die Dateien den Runner verlassen. Jede Datei, die das Gate misst, wird an die AudioLab-API geschickt, die in der EU gehostet wird, und es kommen nur Zahlen zurück. Siehe „Datenschutz und Limits“ unten. Wenn dein Audio deine eigenen Systeme nicht verlassen darf, ist das nicht das richtige Werkzeug für den Build. Die Werkbank misst in deinem Browser und lädt nichts hoch, aber von Hand, nicht in einem Build.
- Wisse, was es kostet. Die zehn Zahlen kommen aus drei Analysen: die Loudness-Zahlen aus einem Aufruf, die Stimmzahlen aus einem zweiten, die Bandbreite aus einem dritten. Das Skript ruft nur die Aufrufe ab, die deine Limits brauchen. Die Limits-Datei unten nutzt alle drei, also kostet eine Datei drei Aufrufe; eine Limits-Datei nur mit Loudness-Zahlen kostet einen Aufruf pro Datei. Jeder Aufruf zählt gegen deine Aufrufe und dein Datenvolumen im Monat. Das Free-Kontingent sind 1.000 Aufrufe im Monat, 2 GB im Monat und 60 Anfragen pro Minute.
- Python 3.9 oder neuer und das Paket:
pip install audiolabtools
Deine Limits
{
"integratedLufs": { "min": -26, "max": -16 },
"truePeakDb": { "max": -1 },
"clipping": { "oneOf": ["None"] },
"signalToNoiseDb": { "min": 30 },
"bandwidthHz": { "min": 7000 }
}
Jeder Eintrag nennt eine der zehn Zahlen und gibt ein min, ein max oder beides an. clipping ist ein Wort, deshalb bekommt es in oneOf eine Liste der Wörter, die du erlaubst. Diese Limits sind Beispiele: Setze deine eigenen aus deinen eigenen guten Aufnahmen. Nimm zwanzig, die du für gut hältst, und nutze ihr 5. und 95. Perzentil, wie es Schritt 7 von „So liest du sie“ im Vergleichsleitfaden beschreibt.
Das Skript
"""Fail a build when an audio file breaks your rules.
python audio_gate.py gate.json audio/intro.wav audio/outro.wav
gate.json holds the limits, one entry per number:
https://audiolab.tools/docs/start/audio-quality-gate-in-ci
Every file is sent to the AudioLab API; only numbers come back.
Exit code: 0 every file passes, 1 a file breaks a rule, 2 a file could not be checked
(a key, a limit of your plan, no network). A build must not pass on 2.
"""
from __future__ import annotations
import json
import sys
import time
from audiolabtools import (
ApiTimeout,
ApiUnreachable,
AudioLab,
AudioLabError,
AuthenticationError,
MissingApiKey,
PlanLimitError,
RateLimitError,
)
PASSED, BROKEN, COULD_NOT_CHECK = 0, 1, 2
# The same for every file, so there is no point in going on: a key that is refused,
# a limit of your plan, a minute that is full, no network.
STOPS = (
AuthenticationError, PlanLimitError, RateLimitError,
MissingApiKey, ApiUnreachable, ApiTimeout,
)
NEAR = " within the margin: the number cannot say if the file is past it"
def broken_rules(numbers, rules):
"""The rules one file breaks, one line each.
A line carries the status and the margin of the number: how far to trust it.
"""
lines = []
for field, rule in rules.items():
m = numbers[field]
trust = (m.status or "no status") + (f", margin {m.margin}" if m.margin else "")
if m.value is None:
lines.append(f"{field} could not be measured")
elif "oneOf" in rule:
if m.value not in rule["oneOf"]:
allowed = ", ".join(rule["oneOf"])
lines.append(f"{field} is {m.value}, allowed: {allowed} ({trust})")
else:
mg = m.margin
slack = max(mg.plus_minus or 0, mg.plus or 0, mg.minus or 0) if mg else 0
low, high = rule.get("min"), rule.get("max")
past = []
if low is not None and m.value < low:
past.append(("below the minimum", low))
if high is not None and m.value > high:
past.append(("above the maximum", high))
for word, limit in past:
lines.append(f"{field} is {m.value} {m.unit}, {word} {limit} ({trust})")
if abs(m.value - limit) <= slack + 1e-9:
lines.append(NEAR)
return lines
def measure(client, path, fields, wait=time.sleep):
"""The numbers of one file. On a full minute the API says how long to wait: do that once."""
try:
return client.ten_numbers(path, only=fields)
except RateLimitError as e:
wait(e.retry_after or 60)
return client.ten_numbers(path, only=fields)
def main(argv, client=None, out=print, wait=time.sleep):
if len(argv) < 3:
out("usage: python audio_gate.py gate.json file [file ...]")
return COULD_NOT_CHECK
with open(argv[1], encoding="utf-8") as f:
rules = json.load(f)
client = client or AudioLab() # reads AUDIOLAB_KEY
result, broken, unchecked = PASSED, 0, 0
for path in argv[2:]:
try:
lines = broken_rules(measure(client, path, list(rules), wait), rules)
except STOPS as e:
out(f"STOP {path}\n {e}")
return COULD_NOT_CHECK
except AudioLabError as e: # a problem with this file only: say so, check the others
out(f"ERROR {path}\n {e}")
unchecked += 1
result = COULD_NOT_CHECK
continue
out(f"{'FAIL' if lines else 'ok '} {path}")
for line in lines:
out(f" {line}")
if lines:
broken += 1
result = max(result, BROKEN)
checked = len(argv) - 2
out(f"{checked} file{'' if checked == 1 else 's'} checked, {broken} break a rule, "
f"{unchecked} could not be checked")
return result
if __name__ == "__main__":
sys.exit(main(sys.argv))
Es liest die Limits, fragt bei ten_numbers die Zahlen ab, die die Limits nennen (only=, damit nichts anderes aufgerufen wird), und gibt pro verletzter Regel eine Zeile aus. Zwei Dinge entscheidet es für dich. Eine volle Minute wird einmal abgewartet, so lange, wie die API es in ihrem Retry-After verlangt, denn das ist es, was die API zu tun empfiehlt. Ein abgelehnter Schlüssel, ein Limit deines Tarifs oder fehlendes Netz stoppt den Lauf bei der ersten Datei, weil dasselbe jeder Datei passieren würde; ein Problem mit einer einzelnen Datei wird gemeldet, und die anderen werden trotzdem geprüft.
Der Workflow
name: audio quality gate
on:
pull_request:
paths: ["audio/**"]
jobs:
gate:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: actions/setup-python@v5
with:
python-version: "3.12"
- run: pip install audiolabtools
- name: Measure the audio files this pull request adds or changes
env:
AUDIOLAB_KEY: ${{ secrets.AUDIOLAB_KEY }}
BASE: ${{ github.base_ref }}
run: |
mapfile -d '' files < <(git diff -z --name-only --diff-filter=AM \
"origin/$BASE...HEAD" -- 'audio/*.wav' 'audio/*.mp3' 'audio/*.flac')
if [ "${#files[@]}" -eq 0 ]; then echo "No audio files changed."; exit 0; fi
python tools/audio_gate.py tools/gate.json "${files[@]}"
Leg audio_gate.py und gate.json in tools/ ab und das Audio in audio/. Der letzte Schritt übergibt dem Skript nur die Dateien, die dieser Pull Request hinzufügt oder ändert, damit eine alte Datei nicht bei jedem Push erneut gemessen wird. Ein Pull Request aus einem Fork bekommt keines deiner Secrets. Ohne Schlüssel läuft das Skript als schlüsselloser Test, der nur 20 Aufrufe im Monat erlaubt, sodass das Gate bald mit einem Limitfehler und Exit-Code 2 stoppt. Entscheide, ob du den Job für Forks überspringst.
So sieht eine fehlgeschlagene Prüfung aus
FAIL audio/prompt-welcome.wav
integratedLufs is -31.5 LUFS, below the minimum -26 (validated, margin ±0.1 LU)
1 file checked, 1 break a rule, 0 could not be checked
Das ist die Ausgabe des Skripts mit den Beispiel-Limits, für eine Datei, die viel zu leise ist. Der Exit-Code ist 1, also scheitert der Build, und das Log sagt, welche Datei, welche Zahl, welches Limit und wie weit du der Zahl trauen kannst.
Liegt eine Zahl näher am Limit als ihre Marge, sagt die Zeile das. Dieselbe Datei gegen ein Minimum von -31,4 statt -26:
FAIL audio/prompt-welcome.wav
integratedLufs is -31.5 LUFS, below the minimum -31.4 (validated, margin ±0.1 LU)
within the margin: the number cannot say if the file is past it
1 file checked, 1 break a rule, 0 could not be checked
So liest du es
- Exit 1 heißt, die Datei hat eine Regel gebrochen. Exit 2 heißt, das Gate konnte es nicht beurteilen. Die Zeile trägt den Fehlercode und die Seite, die ihn erklärt, zum Beispiel
https://audiolab.tools/docs/errors#quota_exceeded. Ein Build darf bei Exit 2 nicht bestehen: Ein Gate, das die API nicht erreicht, hat nichts geprüft. - Lies die Marge, bevor du mit einem Fehlschlag streitest. Eine Zeile mit dem Zusatz „within the margin“ ist eine Zahl, die dir nicht sagen kann, ob die Datei das Limit überschreitet, weil die Zahl selbst um mehr als den Abstand danebenliegen kann. Ist die Marge nicht symmetrisch, nutzt das Skript die größere Seite. Lockere entweder das Limit um die Marge oder repariere die Datei. Das Skript entscheidet das nicht für dich.
- Ein Limit auf einem Proxy vergleicht, es stellt keine Tatsache fest.
speechRatioundlongSilencessind Proxys, und die Zeile sagt das. Sie eignen sich gut, um eine große Veränderung zu erwischen, und schlecht, um eine Nachkommastelle zu überwachen. - Fang locker an, dann zieh an. Lass das Gate eine Weile mit weiten Limits laufen und lies, was es ausgibt, dann verengst du die Limits auf das, was deine guten Aufnahmen zeigen. Ein Gate, das jeden Build scheitern lässt, wird abgeschaltet.
- Ein Lauf misst, was man ihm gibt. Eine Änderung an
gate.jsonmisst die Dateien, die schon im Repository liegen, nicht neu. Führe das Skript nach einer Limit-Änderung einmal über den ganzen Ordner aus.
Was es nicht kann
- Es misst die Datei, nicht, was gesagt wird. Eine Datei innerhalb aller Limits kann trotzdem der falsche Satz, eine schlechte Stimme oder die falsche Sprache sein.
- Es hat kein Gedächtnis. Es vergleicht eine Datei nicht mit einer früheren Version oder mit einer Referenz. Um Anbieter oder Versionen über viele Sätze zu vergleichen, nimm den Vergleichsleitfaden.
- Es läuft nicht offline. Die Engine ist nicht als Paket veröffentlicht, also geht jede Datei an die API.
- Seine Zahlen haben den Status und die Marge, die sie haben. Siehe was AudioLab nicht tut.
Datenschutz und Limits
- Dein Audio wird nicht aufbewahrt. Es wird im Arbeitsspeicher in Frankfurt analysiert. Eine komprimierte Datei (MP3, FLAC, M4A) läuft über eine temporäre Datei, die gleich nach dem Dekodieren gelöscht wird. Eine Datei, die du über den Signed-Upload-Weg hochlädst, wird innerhalb einer Stunde entfernt. Wir behalten die Zahlen und, wenn du es erlaubst, den Dateinamen. Siehe /privacy.
- Limits. Free: 1.000 Aufrufe und 2 GB im Monat, 60 Anfragen pro Minute. Plus: 10.000 Aufrufe, 20 GB, 120 pro Minute. Eine lokale Datei über 4 MB wird vor dem Messen einmal über den Signed-Upload-Weg hochgeladen: Das zählt als Anfrage, aber nicht als Aufruf. Jede akzeptierte Anfrage zählt, auch eine, die danach fehlschlägt. Ein Fehler sagt dir, was zu tun ist: /docs/errors.
- Wo du stehst.
AudioLab().usage()liefert den Tarif sowie die in diesem Monat verbrauchten Aufrufe und Daten gegenüber den Limits. Das Lesen ist kostenlos und zählt nicht als Aufruf.
Weiter in General docs
Einen Datensatz mit Gesprächen prüfen: woher das Audio wirklich stammt →
Miss einen Ordner mit Anruf-, Voice-Agent- oder Text-to-Speech-Aufnahmen auf verlustbehaftete Quellen, Upsampling, Schnittstellen, getarntes Mono, Clipping und Bittiefe, 20 Dateien auf einmal, und wisse, wann das Audio hochgeladen wird und wann nicht.
More in General docs
- Practical
Voice-Agent- und TTS-Anbieter vergleichen und Gesprächsaudio prüfen
Drei API-Aufrufe, zehn Zahlen und wie weit du jeder trauen kannst: ein Leitfaden für QA-Teams, die Voice-Agent-, Text-to-Speech- oder Voice-Isolation-Anbieter vergleichen.
- Intro
Eine Antwort lesen: was jedes Feld bedeutet und wie weit du ihm trauen kannst
Eine echte Antwort der API, Feld für Feld: was sie sagt, ihr Status und ihre Marge und was du daraus schließen darfst und was nicht.
- Intro
Was AudioLab nicht tut
Die Grenzen der Analyse, offen benannt: Es transkribiert nicht, sagt keine Erkennungsfehler voraus, bewertet keine Natürlichkeit, identifiziert keine Sprecher und misst keine Latenz, und seine Hintergrund-Hinweise sind Vermutungen.
- Intro
Änderungen und Deprecation: was sich ändert, wann und wie du es erfährst
Das Versprechen hinter /v1, was als Änderung mit Vorankündigung gilt, wie wir sie ankündigen und was heute veraltet ist.