testingfaces commited on
Commit
0deb27d
Β·
verified Β·
1 Parent(s): 26aa0aa

Update denoiser.py

Browse files
Files changed (1) hide show
  1. denoiser.py +182 -89
denoiser.py CHANGED
@@ -1,66 +1,65 @@
1
  """
2
- Denoiser β€” Cleanvoice API
3
- BUGS FIXED:
4
- 1. cv.process() needs PUBLIC URL not local path
5
- β†’ Upload to Cloudinary first, get URL, send to Cleanvoice
6
- 2. Wrong parameter names (fillers=, stutters=)
7
- β†’ Correct config dict: {'fillers':True,'stutters':True,...}
8
- 3. Wrong SDK call syntax
9
- β†’ cv.process(url, config_dict) not cv.process(path, **kwargs)
10
- 4. Silent error swallowing (except Exception: pass)
11
- β†’ Now logs all errors so you can see what's failing
12
- 5. No stats returned
13
- β†’ Now returns stats dict with noise_method etc.
 
 
 
14
  """
15
 
16
  import os
17
  import re
18
  import time
19
  import logging
20
- import requests
21
  import subprocess
22
- import cloudinary
23
- import cloudinary.uploader
24
 
25
  logger = logging.getLogger(__name__)
26
 
27
- # Cloudinary config β€” set in HF Space secrets
28
- cloudinary.config(
29
- cloud_name = os.environ.get("CLOUD_NAME"),
30
- api_key = os.environ.get("API_KEY"),
31
- api_secret = os.environ.get("API_SECRET"),
32
- )
33
-
34
  CLEANVOICE_API_KEY = os.environ.get(
35
  "CLEANVOICE_API_KEY",
36
  "cvk_Xnmz7ZPXMRg.CcE3YGry3IzkCBWQKGvomBfd9hmC9L-aAvSj-2iNfyw"
37
  )
38
 
39
- # Filler words for transcript cleaning
 
 
 
 
40
  FILLER_PATTERN = r'\b(um|uh|hmm|basically|like|ante|ane|matlab|yani|acha|right|okay)\b'
41
 
42
 
43
  class Denoiser:
44
  def __init__(self):
45
- print("[Denoiser] βœ… Cleanvoice API denoiser ready")
46
-
 
 
 
 
 
 
47
  def process(self, audio_path: str, out_dir: str,
48
  remove_fillers: bool = True,
49
  remove_stutters: bool = True,
50
  remove_silences: bool = True,
51
  **kwargs) -> dict:
52
  """
53
- Process audio using Cleanvoice API.
54
- Steps:
55
- 1. Convert to WAV (16kHz mono)
56
- 2. Upload to Cloudinary β†’ get public URL
57
- 3. Send URL to Cleanvoice API
58
- 4. Download cleaned audio
59
- 5. Return {'audio_path': ..., 'stats': {...}}
60
  """
61
- stats = {"noise_method": "Cleanvoice API"}
62
 
63
- # ── Step 1: Convert to standard WAV ──────────────────────────
64
  standard_wav = os.path.join(out_dir, "input_converted.wav")
65
  result = subprocess.run([
66
  "ffmpeg", "-y", "-i", audio_path,
@@ -72,80 +71,174 @@ class Denoiser:
72
  logger.error(f"[Denoiser] FFmpeg conversion failed: {result.stderr.decode()}")
73
  return {"audio_path": audio_path, "stats": stats}
74
 
75
- # ── Step 2: Upload to Cloudinary β†’ get public URL ─────────────
76
- public_url = None
 
 
 
 
 
 
 
77
  try:
78
- print("[Denoiser] Uploading to Cloudinary for public URL...")
79
- upload_result = cloudinary.uploader.upload(
80
  standard_wav,
81
- resource_type = "video",
82
- folder = "clearwave_input",
 
83
  )
84
- public_url = upload_result["secure_url"]
85
- print(f"[Denoiser] βœ… Uploaded: {public_url}")
86
  except Exception as e:
87
- logger.error(f"[Denoiser] Cloudinary upload failed: {e}")
88
- logger.warning("[Denoiser] Cannot call Cleanvoice without public URL β€” returning original")
89
  return {"audio_path": standard_wav, "stats": stats}
90
 
91
- # ── Step 3: Call Cleanvoice API with correct config ───────────
 
 
 
 
 
 
 
 
 
92
  try:
93
- from cleanvoice import Cleanvoice
94
-
95
- cv = Cleanvoice({"api_key": CLEANVOICE_API_KEY})
96
-
97
- # FIX: Correct config dict format from Cleanvoice docs
98
- config = {
99
- "remove_noise": True,
100
- "normalize": True,
101
- "studio_sound": "nightly", # best quality
102
- }
103
- if remove_fillers:
104
- config["fillers"] = True
105
- config["hesitations"] = True
106
- if remove_stutters:
107
- config["stutters"] = True
108
- if remove_silences:
109
- config["long_silences"] = True
110
-
111
- print(f"[Denoiser] Calling Cleanvoice with config: {config}")
112
-
113
- # FIX: cv.process(PUBLIC_URL, config_dict)
114
- cv_result = cv.process(public_url, config)
115
-
116
- # Download cleaned audio
117
- cleaned_url = cv_result.audio.url
118
- print(f"[Denoiser] βœ… Cleanvoice done: {cleaned_url}")
119
-
120
- audio_data = requests.get(cleaned_url, timeout=120).content
121
  clean_wav = os.path.join(out_dir, "cleanvoice_output.wav")
 
122
  with open(clean_wav, "wb") as f:
123
  f.write(audio_data)
124
-
125
- stats.update({
126
- "noise_method": "Cleanvoice API βœ…",
127
- "fillers_removed": "yes" if remove_fillers else "no",
128
- "stutters_removed": "yes" if remove_stutters else "no",
129
- "silences_removed": "yes" if remove_silences else "no",
130
- })
131
- print("[Denoiser] βœ… Cleanvoice processing complete")
132
- return {"audio_path": clean_wav, "stats": stats}
133
-
134
  except Exception as e:
135
- # FIX: Log the actual error instead of silently ignoring it
136
- logger.error(f"[Denoiser] Cleanvoice API failed: {e}", exc_info=True)
137
- print(f"[Denoiser] ⚠️ Cleanvoice failed ({e}) β€” returning converted WAV")
138
- stats["noise_method"] = f"Cleanvoice failed: {str(e)[:100]}"
139
  return {"audio_path": standard_wav, "stats": stats}
140
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
141
  def clean_transcript_fillers(self, text: str) -> str:
142
  """Remove filler words from transcript text."""
143
  cleaned = re.sub(FILLER_PATTERN, '', text, flags=re.IGNORECASE)
144
- # Clean up extra spaces
145
  cleaned = re.sub(r'\s+', ' ', cleaned).strip()
146
  return cleaned
147
 
148
- # Kept for compatibility β€” not used with Cleanvoice API
149
  def _remove_fillers(self, a, s, seg):
150
  return a, 0
151
 
 
1
  """
2
+ Denoiser β€” Cleanvoice API (direct file upload, NO Cloudinary needed)
3
+
4
+ How it works:
5
+ 1. Convert input audio to 16kHz mono WAV (ffmpeg)
6
+ 2. Upload WAV file directly to Cleanvoice API as multipart/form-data
7
+ 3. Poll Cleanvoice until job is done
8
+ 4. Download and return the cleaned audio
9
+ 5. Return stats dict
10
+
11
+ No Cloudinary, no public URL needed.
12
+ Only requires: CLEANVOICE_API_KEY in HF Space secrets.
13
+
14
+ Cleanvoice REST API:
15
+ POST https://api.cleanvoice.ai/v2/edits β†’ submit job
16
+ GET https://api.cleanvoice.ai/v2/edits/{id} β†’ poll status
17
  """
18
 
19
  import os
20
  import re
21
  import time
22
  import logging
 
23
  import subprocess
24
+ import requests
 
25
 
26
  logger = logging.getLogger(__name__)
27
 
 
 
 
 
 
 
 
28
  CLEANVOICE_API_KEY = os.environ.get(
29
  "CLEANVOICE_API_KEY",
30
  "cvk_Xnmz7ZPXMRg.CcE3YGry3IzkCBWQKGvomBfd9hmC9L-aAvSj-2iNfyw"
31
  )
32
 
33
+ CLEANVOICE_BASE = "https://api.cleanvoice.ai/v2"
34
+ POLL_INTERVAL_SEC = 5 # seconds between status checks
35
+ POLL_TIMEOUT_SEC = 600 # 10 minutes max wait
36
+
37
+ # Filler words removed from transcript text (text-side cleanup)
38
  FILLER_PATTERN = r'\b(um|uh|hmm|basically|like|ante|ane|matlab|yani|acha|right|okay)\b'
39
 
40
 
41
  class Denoiser:
42
  def __init__(self):
43
+ if CLEANVOICE_API_KEY:
44
+ print("[Denoiser] βœ… Cleanvoice API key found β€” ready (no Cloudinary needed)")
45
+ else:
46
+ print("[Denoiser] ⚠️ No CLEANVOICE_API_KEY β€” denoising will be skipped")
47
+
48
+ # ══════════════════════════════════════════════════════════════════
49
+ # PUBLIC
50
+ # ══════════════════════════════════════════════════════════════════
51
  def process(self, audio_path: str, out_dir: str,
52
  remove_fillers: bool = True,
53
  remove_stutters: bool = True,
54
  remove_silences: bool = True,
55
  **kwargs) -> dict:
56
  """
57
+ Process audio using Cleanvoice API (direct file upload).
58
+ Returns {'audio_path': str, 'stats': dict}
 
 
 
 
 
59
  """
60
+ stats = {"noise_method": "Cleanvoice API (direct upload)"}
61
 
62
+ # ── Step 1: Convert to 16kHz mono WAV ────────────────────────
63
  standard_wav = os.path.join(out_dir, "input_converted.wav")
64
  result = subprocess.run([
65
  "ffmpeg", "-y", "-i", audio_path,
 
71
  logger.error(f"[Denoiser] FFmpeg conversion failed: {result.stderr.decode()}")
72
  return {"audio_path": audio_path, "stats": stats}
73
 
74
+ print(f"[Denoiser] βœ… Converted to 16kHz mono WAV")
75
+
76
+ # ── Step 2: Check API key ─────────────────────────────────────
77
+ if not CLEANVOICE_API_KEY:
78
+ logger.warning("[Denoiser] No API key β€” skipping Cleanvoice, returning converted WAV")
79
+ stats["noise_method"] = "Skipped (no API key)"
80
+ return {"audio_path": standard_wav, "stats": stats}
81
+
82
+ # ── Step 3: Submit job via direct file upload ─────────────────
83
  try:
84
+ job_id = self._submit_job(
 
85
  standard_wav,
86
+ remove_fillers=remove_fillers,
87
+ remove_stutters=remove_stutters,
88
+ remove_silences=remove_silences,
89
  )
90
+ print(f"[Denoiser] βœ… Cleanvoice job submitted: {job_id}")
 
91
  except Exception as e:
92
+ logger.error(f"[Denoiser] Job submission failed: {e}", exc_info=True)
93
+ stats["noise_method"] = f"Cleanvoice submit failed: {str(e)[:120]}"
94
  return {"audio_path": standard_wav, "stats": stats}
95
 
96
+ # ── Step 4: Poll until done ───────────────────────────────────
97
+ try:
98
+ download_url = self._poll_job(job_id)
99
+ print(f"[Denoiser] βœ… Cleanvoice done: {download_url}")
100
+ except Exception as e:
101
+ logger.error(f"[Denoiser] Polling failed: {e}", exc_info=True)
102
+ stats["noise_method"] = f"Cleanvoice poll failed: {str(e)[:120]}"
103
+ return {"audio_path": standard_wav, "stats": stats}
104
+
105
+ # ── Step 5: Download cleaned audio ────────────────────────────
106
  try:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
107
  clean_wav = os.path.join(out_dir, "cleanvoice_output.wav")
108
+ audio_data = requests.get(download_url, timeout=120).content
109
  with open(clean_wav, "wb") as f:
110
  f.write(audio_data)
111
+ print(f"[Denoiser] βœ… Cleaned audio saved")
 
 
 
 
 
 
 
 
 
112
  except Exception as e:
113
+ logger.error(f"[Denoiser] Download failed: {e}", exc_info=True)
114
+ stats["noise_method"] = f"Cleanvoice download failed: {str(e)[:120]}"
 
 
115
  return {"audio_path": standard_wav, "stats": stats}
116
 
117
+ stats.update({
118
+ "noise_method": "Cleanvoice API βœ…",
119
+ "fillers_removed": "yes" if remove_fillers else "no",
120
+ "stutters_removed": "yes" if remove_stutters else "no",
121
+ "silences_removed": "yes" if remove_silences else "no",
122
+ })
123
+ return {"audio_path": clean_wav, "stats": stats}
124
+
125
+ # ══════════════════════════════════════════════════════════════════
126
+ # SUBMIT JOB β€” direct multipart file upload (no public URL needed)
127
+ # ══════════════════════════════════════════════════════════════════
128
+ def _submit_job(self, wav_path: str,
129
+ remove_fillers=True,
130
+ remove_stutters=True,
131
+ remove_silences=True) -> str:
132
+ """
133
+ POST /v2/edits with multipart file upload.
134
+ Returns job_id string.
135
+ """
136
+ headers = {"x-api-key": CLEANVOICE_API_KEY}
137
+
138
+ # Build config payload as form fields
139
+ config = {"export_format": "wav"}
140
+ if remove_fillers:
141
+ config["filler_words"] = "true"
142
+ config["hesitations"] = "true"
143
+ if remove_stutters:
144
+ config["stutters"] = "true"
145
+ if remove_silences:
146
+ config["long_silences"] = "true"
147
+
148
+ print(f"[Denoiser] Submitting to Cleanvoice: {config}")
149
+
150
+ with open(wav_path, "rb") as f:
151
+ resp = requests.post(
152
+ f"{CLEANVOICE_BASE}/edits",
153
+ headers=headers,
154
+ files={"audio": (os.path.basename(wav_path), f, "audio/wav")},
155
+ data=config,
156
+ timeout=120,
157
+ )
158
+
159
+ if resp.status_code not in (200, 201, 202):
160
+ raise RuntimeError(
161
+ f"Cleanvoice submit HTTP {resp.status_code}: {resp.text[:300]}"
162
+ )
163
+
164
+ data = resp.json()
165
+
166
+ # Cleanvoice returns { "id": "..." } or { "data": { "id": "..." } }
167
+ job_id = (
168
+ data.get("id")
169
+ or data.get("edit_id")
170
+ or (data.get("data") or {}).get("id")
171
+ )
172
+ if not job_id:
173
+ raise RuntimeError(f"No job ID in Cleanvoice response: {data}")
174
+
175
+ return str(job_id)
176
+
177
+ # ══════════════════════════════════════════════════════════════════
178
+ # POLL JOB
179
+ # ══════════════════════════════════════════════════════════════════
180
+ def _poll_job(self, job_id: str) -> str:
181
+ """
182
+ GET /v2/edits/{job_id} until status == done.
183
+ Returns download URL of cleaned audio.
184
+ """
185
+ headers = {"x-api-key": CLEANVOICE_API_KEY}
186
+ url = f"{CLEANVOICE_BASE}/edits/{job_id}"
187
+ deadline = time.time() + POLL_TIMEOUT_SEC
188
+ attempt = 0
189
+
190
+ while time.time() < deadline:
191
+ attempt += 1
192
+ resp = requests.get(url, headers=headers, timeout=30)
193
+
194
+ if resp.status_code != 200:
195
+ raise RuntimeError(
196
+ f"Cleanvoice poll HTTP {resp.status_code}: {resp.text[:200]}"
197
+ )
198
+
199
+ data = resp.json()
200
+ inner = data.get("data", data) # unwrap nested if present
201
+ status = (inner.get("status") or "").lower()
202
+
203
+ print(f"[Denoiser] Poll #{attempt} β€” status: {status}")
204
+
205
+ if status in ("done", "finished", "completed", "success"):
206
+ # Try all common keys for the output URL
207
+ download_url = (
208
+ inner.get("output_url")
209
+ or inner.get("download_url")
210
+ or inner.get("audio_url")
211
+ or inner.get("url")
212
+ or (inner.get("output") or {}).get("url")
213
+ or (inner.get("result") or {}).get("url")
214
+ )
215
+ if not download_url:
216
+ raise RuntimeError(
217
+ f"Cleanvoice job done but no download URL in response: {inner}"
218
+ )
219
+ return download_url
220
+
221
+ elif status in ("error", "failed", "failure"):
222
+ msg = inner.get("error") or inner.get("message") or str(inner)
223
+ raise RuntimeError(f"Cleanvoice job failed: {msg}")
224
+
225
+ # Still processing β€” wait and retry
226
+ time.sleep(POLL_INTERVAL_SEC)
227
+
228
+ raise TimeoutError(
229
+ f"Cleanvoice job {job_id} did not finish within {POLL_TIMEOUT_SEC}s"
230
+ )
231
+
232
+ # ══════════════════════════════════════════════════════════════════
233
+ # TEXT-SIDE FILLER CLEANUP
234
+ # ══════════════════════════════════════════════════════════════════
235
  def clean_transcript_fillers(self, text: str) -> str:
236
  """Remove filler words from transcript text."""
237
  cleaned = re.sub(FILLER_PATTERN, '', text, flags=re.IGNORECASE)
 
238
  cleaned = re.sub(r'\s+', ' ', cleaned).strip()
239
  return cleaned
240
 
241
+ # Kept for compatibility
242
  def _remove_fillers(self, a, s, seg):
243
  return a, 0
244