Spaces:
Paused
Paused
Download denoiser.py from testingfaces/clearwave-ai: direct link, hf CLI and curl.
- Browser
- Download file 11.5 kB
-
https://huggingface.co/spaces/testingfaces/clearwave-ai/resolve/main/denoiser.py
- Command line
-
hf download hf://spaces/testingfaces/clearwave-ai/denoiser.py
-
curl -L -o denoiser.py https://huggingface.co/spaces/testingfaces/clearwave-ai/resolve/main/denoiser.py
11.5 kB
| """ | |
| Denoiser β Cleanvoice SDK v2 (cleanvoice-sdk) | |
| FIXES APPLIED: | |
| 1. Always convert input to 16kHz mono WAV before uploading to Cleanvoice. | |
| Root cause of HTTP 400: Cleanvoice rejects .aac, .m4a, .ogg, .opus, .flac | |
| at the signed upload URL step β only .mp3 and .wav are reliably accepted. | |
| 2. SDK install check at startup β fails loudly with actionable message. | |
| 3. API key read from CLEANVOICE_API_KEY env var only β no hardcoded key. | |
| 4. Full traceback always printed on failure β never swallowed silently. | |
| 5. Correct SDK v2 parameter names (fillers=, stutters=, long_silences=, | |
| studio_sound="nightly") β v1 names were silently ignored. | |
| 6. output_path= verified; falls back to result.audio.download() if needed. | |
| Install: pip install cleanvoice-sdk | |
| Requires: CLEANVOICE_API_KEY environment variable. | |
| """ | |
| import os | |
| import re | |
| import subprocess | |
| import tempfile | |
| import traceback | |
| import logging | |
| logger = logging.getLogger(__name__) | |
| # ββ API key β env var only, never hardcoded βββββββββββββββββββββββββββββββββββ | |
| CLEANVOICE_API_KEY = os.environ.get("CLEANVOICE_API_KEY", "").strip() | |
| # Filler words for transcript text-side cleanup | |
| FILLER_PATTERN = r'\b(um|uh|hmm|basically|like|ante|ane|matlab|yani|acha|right|okay)\b' | |
| # Formats Cleanvoice reliably accepts for upload (tested) | |
| # Everything else must be converted to WAV first | |
| CLEANVOICE_SAFE_FORMATS = {".mp3", ".wav"} | |
| # ββ SDK import check at module load time ββββββββββββββββββββββββββββββββββββββ | |
| _SDK_AVAILABLE = False | |
| _CleanvoiceClass = None | |
| try: | |
| from cleanvoice import Cleanvoice as _CleanvoiceClass | |
| _SDK_AVAILABLE = True | |
| except ImportError: | |
| pass | |
| class Denoiser: | |
| def __init__(self): | |
| # ββ Check 1: SDK installed? βββββββββββββββββββββββββββββββββββ | |
| if not _SDK_AVAILABLE: | |
| print( | |
| "[Denoiser] β cleanvoice-sdk is NOT installed.\n" | |
| " Fix: pip install cleanvoice-sdk\n" | |
| " Denoising will be skipped until this is resolved." | |
| ) | |
| else: | |
| print("[Denoiser] β cleanvoice-sdk found") | |
| # ββ Check 2: API key set? βββββββββββββββββββββββββββββββββββββ | |
| if not CLEANVOICE_API_KEY: | |
| print( | |
| "[Denoiser] β CLEANVOICE_API_KEY is not set.\n" | |
| " Fix: export CLEANVOICE_API_KEY=your_key_here\n" | |
| " Or add it as a HuggingFace Space Secret.\n" | |
| " Denoising will be skipped until this is resolved." | |
| ) | |
| else: | |
| masked = CLEANVOICE_API_KEY[:4] + "..." + CLEANVOICE_API_KEY[-4:] | |
| print(f"[Denoiser] β API key found ({masked})") | |
| if _SDK_AVAILABLE and CLEANVOICE_API_KEY: | |
| print("[Denoiser] β Ready β Cleanvoice SDK v2 will process audio") | |
| # ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # PUBLIC | |
| # ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def process(self, audio_path: str, out_dir: str, | |
| remove_fillers: bool = True, | |
| remove_stutters: bool = True, | |
| remove_silences: bool = True, | |
| **kwargs) -> dict: | |
| """ | |
| Process audio using Cleanvoice SDK v2. | |
| Returns {'audio_path': str, 'stats': dict}. | |
| On failure, returns original audio_path so the rest of the pipeline | |
| (transcription, translation) can still run. | |
| """ | |
| stats = {"noise_method": "none"} | |
| # ββ Pre-flight checks βββββββββββββββββββββββββββββββββββββββββ | |
| if not _SDK_AVAILABLE: | |
| msg = "Skipped β cleanvoice-sdk not installed (run: pip install cleanvoice-sdk)" | |
| print(f"[Denoiser] β οΈ {msg}") | |
| stats["noise_method"] = msg | |
| return {"audio_path": audio_path, "stats": stats} | |
| if not CLEANVOICE_API_KEY: | |
| msg = "Skipped β CLEANVOICE_API_KEY not set" | |
| print(f"[Denoiser] β οΈ {msg}") | |
| stats["noise_method"] = msg | |
| return {"audio_path": audio_path, "stats": stats} | |
| # ββ Step 1: Convert to WAV if needed βββββββββββββββββββββββββ | |
| # Cleanvoice returns HTTP 400 for .aac, .m4a, .ogg, .opus, .flac | |
| # at the signed upload step. Always convert anything that isn't | |
| # .mp3 or .wav to a clean 16kHz mono WAV before uploading. | |
| upload_path, tmp_wav = self._ensure_wav(audio_path, out_dir) | |
| # ββ Step 2: Cleanvoice SDK v2 call ββββββββββββββββββββββββββββ | |
| try: | |
| client = _CleanvoiceClass(api_key=CLEANVOICE_API_KEY) | |
| ext = os.path.splitext(upload_path)[1] or ".wav" | |
| clean_out = os.path.join(out_dir, f"cleanvoice_output{ext}") | |
| file_kb = os.path.getsize(upload_path) / 1024 | |
| print(f"[Denoiser] Sending to Cleanvoice v2: {upload_path} ({file_kb:.1f} KB)") | |
| # SDK v2 correct parameter names | |
| # WRONG (v1, silently broken): | |
| # remove_fillers=, remove_stutters=, remove_silences=, studio_sound=True | |
| # CORRECT (v2): | |
| # fillers=, stutters=, long_silences=, studio_sound="nightly" | |
| result = client.process( | |
| upload_path, | |
| remove_noise=True, | |
| studio_sound="nightly", # must be "nightly", NOT True | |
| normalize=True, | |
| fillers=remove_fillers, # was: remove_fillers= (v1, ignored) | |
| stutters=remove_stutters, # was: remove_stutters= (v1, ignored) | |
| long_silences=remove_silences, # was: remove_silences= (v1, ignored) | |
| mouth_sounds=True, | |
| breath=True, | |
| output_path=clean_out, | |
| ) | |
| # output_path= is honoured in most SDK builds, but not all β | |
| # fall back to result.audio.download() if the file wasn't written | |
| if not os.path.exists(clean_out): | |
| print("[Denoiser] output_path not written β trying result.audio.download()") | |
| result.audio.download(clean_out) | |
| if not os.path.exists(clean_out) or os.path.getsize(clean_out) == 0: | |
| raise RuntimeError("Cleanvoice returned an empty or missing output file") | |
| # Clean up temp WAV if we created one | |
| if tmp_wav and os.path.exists(tmp_wav): | |
| os.unlink(tmp_wav) | |
| out_kb = os.path.getsize(clean_out) / 1024 | |
| print(f"[Denoiser] β Cleanvoice v2 done β {clean_out} ({out_kb:.1f} KB)") | |
| stats.update({ | |
| "noise_method": "Cleanvoice SDK v2 β ", | |
| "fillers_removed": "yes" if remove_fillers else "no", | |
| "stutters_removed": "yes" if remove_stutters else "no", | |
| "silences_removed": "yes" if remove_silences else "no", | |
| }) | |
| return {"audio_path": clean_out, "stats": stats} | |
| except Exception as e: | |
| # Clean up temp WAV on failure too | |
| if tmp_wav and os.path.exists(tmp_wav): | |
| try: | |
| os.unlink(tmp_wav) | |
| except Exception: | |
| pass | |
| # Print the FULL traceback β never hide it | |
| print("\n[Denoiser] β Cleanvoice v2 FAILED β full traceback below:") | |
| traceback.print_exc() | |
| print(f"[Denoiser] β Error: {type(e).__name__}: {e}\n") | |
| logger.error("[Denoiser] Cleanvoice v2 failed", exc_info=True) | |
| stats["noise_method"] = f"Cleanvoice FAILED ({type(e).__name__}): {str(e)[:200]}" | |
| return {"audio_path": audio_path, "stats": stats} | |
| # ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # FORMAT CONVERSION | |
| # ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def _ensure_wav(self, audio_path: str, out_dir: str): | |
| """ | |
| Converts audio to 16kHz mono WAV if it's not already in a format | |
| Cleanvoice reliably accepts (.mp3 or .wav). | |
| Returns (path_to_upload, temp_wav_path_or_None). | |
| Caller must delete temp_wav_path after the API call. | |
| """ | |
| ext = os.path.splitext(audio_path)[1].lower() | |
| if ext in CLEANVOICE_SAFE_FORMATS: | |
| print(f"[Denoiser] Format {ext} β no conversion needed") | |
| return audio_path, None | |
| # Convert to 16kHz mono WAV β safest format for Cleanvoice upload | |
| wav_path = os.path.join(out_dir, "cleanvoice_input.wav") | |
| print(f"[Denoiser] Converting {ext} β WAV for Cleanvoice upload...") | |
| try: | |
| r = subprocess.run([ | |
| "ffmpeg", "-y", "-i", audio_path, | |
| "-vn", # strip video if present | |
| "-ar", "16000", # 16kHz β Cleanvoice's preferred rate | |
| "-ac", "1", # mono | |
| "-acodec", "pcm_s16le", # uncompressed PCM β always accepted | |
| wav_path | |
| ], capture_output=True) | |
| if r.returncode != 0 or not os.path.exists(wav_path): | |
| stderr = r.stderr.decode("utf-8", errors="replace") | |
| raise RuntimeError(f"ffmpeg conversion failed: {stderr[-300:]}") | |
| kb = os.path.getsize(wav_path) / 1024 | |
| print(f"[Denoiser] β Converted to WAV: {wav_path} ({kb:.1f} KB)") | |
| return wav_path, wav_path | |
| except Exception as e: | |
| print(f"[Denoiser] β οΈ WAV conversion failed ({e}) β trying original file") | |
| return audio_path, None | |
| # ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # TEXT-SIDE FILLER CLEANUP | |
| # ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def clean_transcript_fillers(self, text: str) -> str: | |
| """Remove filler words from transcript text.""" | |
| cleaned = re.sub(FILLER_PATTERN, '', text, flags=re.IGNORECASE) | |
| cleaned = re.sub(r'\s+', ' ', cleaned).strip() | |
| return cleaned | |
| # Kept for backwards compatibility | |
| def _remove_fillers(self, a, s, seg): | |
| return a, 0 | |
| def _remove_stutters(self, a, s, seg): | |
| return a, 0 |