/docs · General · 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.
Mis à jour 12 oct. 2026
À qui s’adresse ce guide
Vous sortez de l’audio d’un pipeline : invites de synthèse vocale, enregistrements d’agents vocaux, épisodes de podcast, sons de jeu ou d’application. Un build doit s’arrêter quand un fichier est trop faible, écrête, est limité en bande passante ou bruyant, avant qu’un auditeur l’entende.
Il s’adresse aux équipes qui fixent elles-mêmes leurs limites. AudioLab mesure le fichier ; c’est votre règle qui décide si un chiffre passe. Il ne juge pas le naturel d’une voix et ne lit pas les mots. Voir ce que AudioLab ne fait pas.
Ce que vous obtenez
- Un script,
audio_gate.py, qui mesure chaque fichier que vous lui donnez, vérifie vos limites et se termine par un code que votre CI comprend. Sa seule dépendance est le paquetaudiolabtools. - Un fichier de limites,
gate.json, avec une entrée par chiffre. - Un workflow,
audio-gate.yml, qui lance le script sur les pull requests et ne mesure que les fichiers audio que la pull request ajoute ou modifie. - Chaque ligne en échec nomme le chiffre, sa valeur et la limite, et dit jusqu’où on peut faire confiance au chiffre : son statut et sa marge. Celui qui lit un build rouge sait ce qu’il a sous les yeux. Voir comment lire une réponse.
Avant de commencer
- Obtenez une clé. Elle est gratuite et ne demande pas de carte : connectez-vous sur /account et générez-en une. Dans GitHub, stockez-la comme secret de dépôt nommé
AUDIOLAB_KEY. Ne la commitez jamais. - Sachez que les fichiers quittent le runner. Chaque fichier que le contrôle mesure est envoyé à l’API AudioLab, hébergée dans l’UE, et seuls des chiffres reviennent. Voir « Confidentialité et limites » plus bas. Si votre audio n’a pas le droit de quitter vos propres systèmes, ce n’est pas l’outil pour ce build. L’espace de travail mesure dans votre navigateur et ne téléverse rien, mais à la main, pas dans un build.
- Sachez ce que cela coûte. Les dix chiffres viennent de trois analyses : les chiffres de loudness d’un premier appel, les chiffres de voix d’un deuxième, la bande passante d’un troisième. Le script ne demande que les appels dont vos limites ont besoin. Le fichier de limites ci-dessous utilise les trois, donc un fichier coûte trois appels ; un fichier de limites avec seulement des chiffres de loudness coûte un appel par fichier. Chaque appel est décompté de vos appels et de vos données du mois. Le palier gratuit est de 1 000 appels par mois, 2 Go par mois et 60 requêtes par minute.
- Python 3.9 ou plus récent, et le paquet :
pip install audiolabtools
Vos limites
{
"integratedLufs": { "min": -26, "max": -16 },
"truePeakDb": { "max": -1 },
"clipping": { "oneOf": ["None"] },
"signalToNoiseDb": { "min": 30 },
"bandwidthHz": { "min": 7000 }
}
Chaque entrée nomme l’un des dix chiffres et donne un min, un max ou les deux. clipping est un mot, il reçoit donc une liste des mots que vous autorisez dans oneOf. Ces limites sont des exemples : fixez les vôtres à partir de vos propres bons enregistrements. Prenez-en vingt que vous jugez bons et utilisez leur 5e et leur 95e percentile, comme le dit l’étape 7 de « Comment les lire » dans le guide de comparaison.
Le script
"""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))
Il lit les limites, demande à ten_numbers les chiffres que les limites nomment (only=, donc rien d’autre n’est appelé) et affiche une ligne par règle enfreinte. Deux choses qu’il décide pour vous. Une minute pleine est attendue une seule fois, aussi longtemps que l’API le demande dans son Retry-After, parce que c’est ce que l’API dit de faire. Une clé refusée, une limite de votre offre ou l’absence de réseau arrête l’exécution dès le premier fichier, car la même chose arriverait à chaque fichier ; un problème sur un seul fichier est signalé et les autres sont tout de même contrôlés.
Le 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[@]}"
Placez audio_gate.py et gate.json dans tools/, et l’audio dans audio/. La dernière étape ne transmet au script que les fichiers que cette pull request ajoute ou modifie, de sorte qu’un ancien fichier n’est pas mesuré de nouveau à chaque push. Une pull request venant d’un fork n’obtient aucun de vos secrets. Sans clé, le script tourne en essai sans clé, qui n’autorise que 20 appels par mois : le contrôle s’arrête donc vite sur une erreur de limite et le code de sortie 2. Décidez si vous sautez le job pour les forks.
À quoi ressemble un contrôle en échec
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
C’est la sortie du script avec les limites d’exemple, pour un fichier beaucoup trop faible. Le code de sortie est 1, le build échoue donc, et le journal dit quel fichier, quel chiffre, quelle limite et jusqu’où on peut faire confiance au chiffre.
Quand un chiffre est plus proche de la limite que sa marge, la ligne le dit. Le même fichier face à un minimum de -31,4 au lieu de -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
Comment le lire
- Le code 1 signifie que le fichier a enfreint une règle. Le code 2 signifie que le contrôle n’a pas pu trancher. La ligne porte le code d’erreur et la page qui l’explique, par exemple
https://audiolab.tools/docs/errors#quota_exceeded. Un build ne doit pas passer sur le code 2 : un contrôle qui ne peut pas joindre l’API n’a rien vérifié. - Lisez la marge avant de contester un échec. Une ligne suivie de « within the margin » est un chiffre qui ne peut pas dire si le fichier dépasse la limite, parce que le chiffre lui-même peut être faux de plus que la distance. Quand la marge n’est pas symétrique, le script prend le plus grand côté. Soit vous assouplissez la limite de la valeur de la marge, soit vous corrigez le fichier. Le script ne décide pas à votre place.
- Une limite sur un proxy compare, elle n’énonce pas un fait.
speechRatioetlongSilencessont des proxys, et la ligne le dit. Ils sont bons pour repérer un gros changement et médiocres pour surveiller une décimale. - Commencez large, puis resserrez. Lancez le contrôle un moment avec des limites larges et lisez ce qu’il affiche, puis resserrez les limites sur ce que montrent vos bons enregistrements. Un contrôle qui fait échouer chaque build finit désactivé.
- Une exécution mesure ce qu’on lui donne. Modifier
gate.jsonne mesure pas les fichiers déjà présents dans le dépôt. Après avoir changé une limite, lancez le script une fois sur tout le dossier.
Ce qu’il ne peut pas faire
- Il mesure le fichier, pas ce qui est dit. Un fichier dans toutes les limites peut tout de même être la mauvaise phrase, une mauvaise voix ou la mauvaise langue.
- Il n’a pas de mémoire. Il ne compare pas un fichier à une version antérieure ni à une référence. Pour comparer des fournisseurs ou des versions sur de nombreuses phrases, utilisez le guide de comparaison.
- Il ne fonctionne pas hors ligne. Le moteur n’est pas publié comme paquet, donc chaque fichier va à l’API.
- Ses chiffres ont le statut et la marge qu’ils ont. Voir ce que AudioLab ne fait pas.
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 fichier local de plus de 4 Mo est téléversé une fois par la voie d’envoi signé avant d’être mesuré : cela compte comme une requête mais pas comme un appel. Chaque requête acceptée compte, y compris celle qui échoue ensuite. Une erreur vous dit quoi faire : /docs/errors.
- 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. La lecture est gratuite et ne compte pas comme un appel.
Ensuite dans General docs
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.
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.
- Intro
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.
- 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.