Spaces:
Running
Running
| """ | |
| 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() | |