ManuBo's picture
Fix Hugging Face Space config at repo root
655ea57
Raw
History Blame Contribute Delete
14.1 kB
"""
audio-data-quality-toolkit -- HuggingFace Space
Upload audio files and get instant quality reports.
No GPU required. Runs entirely on CPU.
"""
from __future__ import annotations
import json
from pathlib import Path
import gradio as gr
import sys
sys.path.insert(0, str(Path(__file__).parent.parent))
from audio_qa.pipeline import check_file
from audio_qa.checks.transcript_ratio import check_transcript_ratio
def analyze_single(audio_path, transcript, expected_sr, min_dur, max_dur, snr_thresh):
if audio_path is None:
return "Upload an audio file to get started.", ""
expected = int(expected_sr) if expected_sr and expected_sr != "Auto" else None
result = check_file(
audio_path,
expected_sr=expected,
min_duration=float(min_dur),
max_duration=float(max_dur),
snr_threshold=float(snr_thresh),
)
if result.get("error"):
return "Error: %s" % result["error"], ""
if transcript and transcript.strip():
tr_check = check_transcript_ratio(result["duration_s"], transcript)
result["checks"].append(tr_check)
result["num_checks"] = len(result["checks"])
result["num_passed"] = sum(1 for c in result["checks"] if c.get("passed"))
result["all_passed"] = result["num_passed"] == result["num_checks"]
lines = []
status = "PASS" if result["all_passed"] else "ISSUES FOUND"
score = result.get("quality_score", 0)
grade = result.get("grade", "?")
lines.append("## %s -- Quality Score: %.1f / 10 (Grade %s)" % (status, score, grade))
lines.append("")
lines.append("**Duration:** %.2fs | **Sample Rate:** %d Hz | **Checks:** %d/%d passed" % (
result["duration_s"], result["sample_rate"],
result["num_passed"], result["num_checks"],
))
lines.append("")
components = result.get("score_components", {})
if components:
lines.append("### Score Breakdown")
lines.append("")
for comp_name, comp_score in components.items():
bar_len = int(comp_score)
bar = "=" * bar_len + "." * (10 - bar_len)
lines.append("- **%s:** %s %.1f" % (comp_name.title(), bar, comp_score))
lines.append("")
for c in result["checks"]:
name = c["check"].replace("_", " ").title()
if c.get("passed"):
lines.append("- [PASS] **%s**" % name)
else:
severity = c.get("severity", "medium")
icon = "FAIL" if severity == "high" else "WARN"
detail_parts = []
for k, v in c.items():
if k in ("check", "passed", "severity"):
continue
if isinstance(v, list) and v:
detail_parts.append("%s: %s" % (k, "; ".join(str(x) for x in v)))
elif isinstance(v, float):
detail_parts.append("%s=%.2f" % (k, v))
elif v is not None and not isinstance(v, list):
detail_parts.append("%s=%s" % (k, v))
detail = " | ".join(detail_parts[:4])
lines.append("- [%s] **%s**: %s" % (icon, name, detail))
summary = "\n".join(lines)
json_output = json.dumps(result, indent=2, default=str)
return summary, json_output
def analyze_batch(files, expected_sr, min_dur, max_dur, snr_thresh):
if not files:
return "Upload one or more audio files."
expected = int(expected_sr) if expected_sr and expected_sr != "Auto" else None
all_results = []
for f in files:
filepath = f.name if hasattr(f, 'name') else str(f)
result = check_file(
filepath,
expected_sr=expected,
min_duration=float(min_dur),
max_duration=float(max_dur),
snr_threshold=float(snr_thresh),
)
all_results.append(result)
n_total = len(all_results)
n_clean = sum(1 for r in all_results if r.get("all_passed"))
n_error = sum(1 for r in all_results if r.get("error"))
check_stats = {}
for r in all_results:
for c in r.get("checks", []):
name = c["check"]
if name not in check_stats:
check_stats[name] = {"passed": 0, "failed": 0}
if c.get("passed"):
check_stats[name]["passed"] += 1
else:
check_stats[name]["failed"] += 1
lines = []
lines.append("## Dataset Report")
lines.append("")
scores = [r.get("quality_score", 0) for r in all_results
if r.get("quality_score") is not None and not r.get("error")]
avg_score = sum(scores) / max(len(scores), 1) if scores else 0
lines.append("**Total:** %d files | **Clean:** %d (%.0f%%) | **Avg Score:** %.1f/10 | **Errors:** %d" % (
n_total, n_clean, n_clean / max(n_total, 1) * 100,
avg_score, n_error,
))
lines.append("")
lines.append("### Per-Check Pass Rates")
lines.append("")
lines.append("| Check | Passed | Failed | Rate |")
lines.append("|-------|--------|--------|------|")
for name, counts in check_stats.items():
total = counts["passed"] + counts["failed"]
rate = counts["passed"] / max(total, 1) * 100
lines.append("| %s | %d | %d | %.0f%% |" % (
name.replace("_", " ").title(), counts["passed"], counts["failed"], rate
))
lines.append("")
lines.append("### Flagged Files")
lines.append("")
for r in all_results:
if r.get("error"):
lines.append("- **ERROR** %s: %s" % (Path(r["file"]).name, r["error"]))
elif not r.get("all_passed"):
fname = Path(r["file"]).name
score = r.get("quality_score", 0)
grade = r.get("grade", "?")
failed = [c["check"] for c in r.get("checks", []) if not c.get("passed")]
lines.append("- **%s** (%.1f/10 %s): %s" % (fname, score, grade, ", ".join(failed)))
return "\n".join(lines)
TITLE = "🎙️ Audio Data Quality Toolkit for TTS/ASR Training Pipelines"
DESCRIPTION = """
Detect clipping, silence, noisy samples, duplicate clips, transcript mismatch,
speaker imbalance, and synthetic-data artifacts in speech datasets.
Designed for TTS, ASR, voice-cloning, and synthetic speech evaluation workflows.
"""
with gr.Blocks(title="Audio Data Quality Toolkit", theme=gr.themes.Soft()) as demo:
gr.Markdown(f"# {TITLE}")
gr.Markdown(DESCRIPTION)
gr.Markdown("""
**Lint your audio datasets before training.** Training-readiness checks for TTS, ASR, and voice-cloning pipelines, with roadmap support for duplicate detection, speaker balance, and ASR-based transcript alignment.
No GPU required. All checks run on CPU with numpy/scipy/librosa.
Unlike perceptual scoring tools such as NISQA, PESQ, or UTMOS, which answer *"how good does this sound?"*,
this toolkit answers *"is this file ready for training?"* by catching the data-engineering issues that silently degrade model quality.
""")
with gr.Tabs():
with gr.Tab("Single clip analysis"):
gr.Markdown("Upload one audio clip and inspect training-readiness quality signals.")
with gr.Row():
with gr.Column(scale=2):
audio_input = gr.Audio(type="filepath", label="Upload audio clip")
transcript_input = gr.Textbox(
label="Optional transcript",
placeholder="Paste the expected transcript here to check chars-per-second alignment...",
lines=2,
)
with gr.Column(scale=1):
sr_choice = gr.Dropdown(
choices=["Auto", "16000", "22050", "24000", "44100", "48000"],
value="Auto", label="Expected sample rate",
)
min_dur = gr.Number(value=0.5, label="Min duration (s)")
max_dur = gr.Number(value=30.0, label="Max duration (s)")
snr_thresh = gr.Number(value=20.0, label="SNR threshold (dB)")
analyze_btn = gr.Button("Analyze audio quality", variant="primary")
result_md = gr.Markdown(label="Quality report")
result_json = gr.Code(label="Full JSON", language="json")
analyze_btn.click(
analyze_single,
inputs=[audio_input, transcript_input, sr_choice, min_dur, max_dur, snr_thresh],
outputs=[result_md, result_json],
)
with gr.Tab("Batch dataset audit"):
gr.Markdown(
"Upload multiple clips to generate a dataset-level QA report for TTS, ASR, voice-cloning, or synthetic speech pipelines."
)
batch_input = gr.File(
file_count="multiple",
file_types=["audio"],
label="Upload audio files",
)
with gr.Row():
b_sr = gr.Dropdown(
choices=["Auto", "16000", "22050", "24000", "44100", "48000"],
value="Auto", label="Expected sample rate",
)
b_min = gr.Number(value=0.5, label="Min duration (s)")
b_max = gr.Number(value=30.0, label="Max duration (s)")
b_snr = gr.Number(value=20.0, label="SNR threshold (dB)")
batch_btn = gr.Button("Run batch audit", variant="primary")
batch_result = gr.Markdown(label="Dataset quality report")
batch_btn.click(
analyze_batch,
inputs=[batch_input, b_sr, b_min, b_max, b_snr],
outputs=[batch_result],
)
with gr.Tab("Synthetic speech evaluation"):
gr.Markdown(
"Evaluate generated speech samples for clipping, silence, noise, duration anomalies, and transcript consistency."
)
with gr.Row():
with gr.Column(scale=2):
synthetic_audio = gr.Audio(type="filepath", label="Generated speech sample")
expected_text = gr.Textbox(
label="Expected text",
placeholder="Paste the prompt/text that the TTS system was supposed to speak...",
lines=3,
)
with gr.Column(scale=1):
synth_sr = gr.Dropdown(
choices=["Auto", "16000", "22050", "24000", "44100", "48000"],
value="Auto", label="Expected sample rate",
)
synth_min = gr.Number(value=0.5, label="Min duration (s)")
synth_max = gr.Number(value=60.0, label="Max duration (s)")
synth_snr = gr.Number(value=20.0, label="SNR threshold (dB)")
synth_button = gr.Button("Evaluate synthetic sample", variant="primary")
synth_output = gr.Markdown(label="Synthetic speech QA")
synth_json = gr.Code(label="Full JSON", language="json")
synth_button.click(
analyze_single,
inputs=[synthetic_audio, expected_text, synth_sr, synth_min, synth_max, synth_snr],
outputs=[synth_output, synth_json],
)
with gr.Tab("About"):
gr.Markdown("""
## What this tool checks
- **Clipping:** waveform peaks too close to maximum amplitude
- **Silence:** long leading, trailing, or internal silent regions
- **Noise:** low signal quality, background hum, hiss, or abnormal energy profile
- **Transcript mismatch:** audio duration may not match the expected text length
- **Speaker imbalance:** some speakers may dominate the dataset *(roadmap / metadata-dependent)*
- **Duplicates:** repeated or near-identical clips *(roadmap / fingerprinting-dependent)*
- **Synthetic artifacts:** robotic, metallic, repeated, or degraded generated speech patterns
## Why this matters
Data quality directly affects TTS/ASR model stability, pronunciation, speaker consistency, alignment, and long-form generation quality.
This Space is designed as a practical QA dashboard for speech datasets used in training and evaluating voice AI systems.
## Current checks
| # | Check | What It Catches | GPU? |
|---|-------|----------------|------|
| 1 | SNR Estimation | Background noise, hum, hiss | No |
| 2 | Clipping Detection | Consecutive samples at max amplitude | No |
| 3 | Silence Analysis | Excessive leading, trailing, or internal silence | No |
| 4 | Sample Rate Validation | Non-standard or unexpected rates | No |
| 5 | Duration Bounds | Too short or too long clips | No |
| 6 | Loudness (LUFS) | Audio far from target loudness | No |
| 7 | Metallic Artifacts | Robotic/metallic TTS artifacts | No |
| 8 | Repetition Detection | Word/phrase loops via autocorrelation | No |
| 9 | Channel Issues | Stereo, silent channels, phase inversion | No |
| 10 | Upsampling Detection | Fake sample rates, e.g. 8kHz upsampled to 22kHz | No |
| 11 | Transcript Ratio | Misaligned transcripts using chars-per-second | No |
| 12 | Duplicate Detection | Near-duplicate files via fingerprinting | No |
| 13 | Transcript Alignment | Audio vs text mismatch with optional ASR | Optional |
## How is this different from NISQA / PESQ / DataSpeech?
| Tool | What it does | GPU | Output |
|------|-------------|-----|--------|
| **NISQA** | Perceptual MOS score | Yes | Quality score |
| **PESQ** | Reference-based quality score | No | Quality score |
| **DataSpeech** | Annotate datasets for Parler-TTS training | Yes | Natural-language descriptions |
| **This toolkit** | Pass/fail lint for training readiness | No | Report + clean manifest |
DataSpeech answers: *"describe this audio's characteristics for TTS conditioning."*
This toolkit answers: *"should I include this file in my training set at all?"*
---
**Install:** `pip install audio-data-quality-toolkit`
[GitHub](https://github.com/EmmanuelleB985/audio-data-quality-toolkit)
**Python API:** `from audio_qa import check_file, check_directory, audit_hf_dataset`
""")
if __name__ == "__main__":
demo.launch()