Clearwave48 commited on
Commit
8a41adc
Β·
verified Β·
1 Parent(s): cfcf5ed

Update main.py

Browse files
Files changed (1) hide show
  1. main.py +91 -132
main.py CHANGED
@@ -1,18 +1,16 @@
1
  """
2
  ClearWave AI β€” API Space (FastAPI)
3
  ===================================
4
- Endpoints: /api/health | /api/process-url
5
 
6
  Pipeline:
7
- 1. Download audio from URL
8
- 2. Denoise / enhance β†’ Denoiser (Cleanvoice SDK)
9
- 3. Transcribe β†’ Groq Whisper large-v3 (primary) / faster-whisper (fallback)
10
- 4. Translate β†’ NLLB-200-1.3B (primary) / Google Translate (fallback)
11
- 5. Summarize β†’ Extractive (position-scored)
12
- 6. Upload result β†’ Cloudinary
13
-
14
- All secrets read from HF Space environment variables:
15
- CLEANVOICE_API_KEY, CLOUD_NAME, API_KEY, API_SECRET, GROQ_API_KEY
16
  """
17
 
18
  import os
@@ -27,27 +25,26 @@ from fastapi import FastAPI, Request
27
  from fastapi.responses import StreamingResponse, JSONResponse
28
  from fastapi.middleware.cors import CORSMiddleware
29
 
30
- from denoiser import Denoiser
31
- from transcriber import Transcriber
32
- from translator import Translator
33
 
34
- # ── Cloudinary config ──────────────────────────────────────────────────────────
35
  cloudinary.config(
36
- cloud_name = os.environ.get("CLOUD_NAME"),
37
- api_key = os.environ.get("API_KEY"),
38
- api_secret = os.environ.get("API_SECRET"),
39
  )
40
 
41
  logging.basicConfig(level=logging.INFO)
42
  logger = logging.getLogger(__name__)
43
 
44
- # ── Singleton instances (loaded once at startup) ───────────────────────────────
45
- denoiser = Denoiser()
46
  transcriber = Transcriber()
47
- translator = Translator()
48
 
49
  app = FastAPI(title="ClearWave AI API")
50
-
51
  app.add_middleware(
52
  CORSMiddleware,
53
  allow_origins=["*"],
@@ -56,82 +53,61 @@ app.add_middleware(
56
  )
57
 
58
 
59
- # ══════════════════════════════════════════════════════════════════════════════
60
- # PIPELINE
61
- # ══════════════════════════════════════════════════════════════════════════════
62
-
63
  def run_pipeline(audio_path, src_lang="auto", tgt_lang="te",
64
- opt_fillers=True, opt_stutters=True, opt_silences=True,
65
- opt_breaths=True, opt_mouth=True):
66
  """
67
  Generator β€” yields SSE-style dicts at each step.
68
- Caller wraps each dict in "data: <json>\n\n"
69
  """
70
- out_dir = tempfile.mkdtemp()
71
- stats = {}
72
  word_segs = []
 
73
 
74
  try:
75
- # ── Step 1: Cleanvoice β€” full audio enhancement ───────────────────────
76
  yield {"status": "processing", "step": 1,
77
  "message": "Step 1/4 β€” Enhancing audio with Cleanvoice..."}
78
- try:
79
- result = denoiser.process(
80
- audio_path, out_dir,
81
- fillers=opt_fillers,
82
- stutters=opt_stutters,
83
- long_silences=opt_silences,
84
- breaths=opt_breaths,
85
- mouth_sounds=opt_mouth,
86
- )
87
- clean1 = result["audio_path"]
88
- stats = {
89
- "noise_method": "Cleanvoice API",
90
- "fillers_removed": "yes" if opt_fillers else "no",
91
- "stutters_removed": "yes" if opt_stutters else "no",
92
- "silences_removed_sec": "yes" if opt_silences else "no",
93
- "breaths_reduced": opt_breaths,
94
- "mouth_sounds_removed": "yes" if opt_mouth else "no",
95
- }
96
- logger.info("[Pipeline] Cleanvoice enhancement complete")
97
- except Exception as e:
98
- # Cleanvoice failed β€” log it and continue with original audio
99
- logger.error(f"[Pipeline] Cleanvoice failed: {e} β€” using original audio")
100
- clean1 = audio_path
101
- stats = {
102
- "noise_method": f"Cleanvoice failed: {e}",
103
- "fillers_removed": 0,
104
- "stutters_removed": 0,
105
- "silences_removed_sec": 0,
106
- "breaths_reduced": False,
107
- "mouth_sounds_removed": 0,
108
- }
109
-
110
- # ── Step 2: Transcribe ────────────────────────────────────────────────
111
  yield {"status": "processing", "step": 2,
112
  "message": "Step 2/4 β€” Transcribing..."}
113
- transcript, detected_lang, t_method = transcriber.transcribe(clean1, src_lang)
114
  word_segs = transcriber._last_segments
115
 
116
- # Clean filler words from transcript text too
117
  if opt_fillers:
118
  transcript = denoiser.clean_transcript_fillers(transcript)
119
 
120
- logger.info(f"[Pipeline] Transcription done: {len(transcript.split())} words, lang={detected_lang}")
121
 
122
- # ── Step 3: Translate ─────────────────────────────────────────────────
123
  translation = transcript
124
- tl_method = "same language"
125
  if tgt_lang != "auto" and detected_lang != tgt_lang:
126
  yield {"status": "processing", "step": 3,
127
  "message": "Step 3/4 β€” Translating..."}
128
  translation, tl_method = translator.translate(transcript, detected_lang, tgt_lang)
129
- logger.info(f"[Pipeline] Translation done via {tl_method}")
130
  else:
131
  yield {"status": "processing", "step": 3,
132
- "message": "Step 3/4 β€” Skipping translation (same language)..."}
133
 
134
- # ── Step 4: Summarize + upload to Cloudinary ──────────────────────────
135
  yield {"status": "processing", "step": 4,
136
  "message": "Step 4/4 β€” Summarizing & uploading..."}
137
  summary = translator.summarize(transcript)
@@ -139,47 +115,40 @@ def run_pipeline(audio_path, src_lang="auto", tgt_lang="te",
139
  enhanced_url = None
140
  try:
141
  upload_result = cloudinary.uploader.upload(
142
- clean1,
143
- resource_type="video", # Cloudinary uses "video" for audio files
144
  folder="clearwave_enhanced",
145
  )
146
  enhanced_url = upload_result["secure_url"]
147
- logger.info(f"[Pipeline] Cloudinary upload done: {enhanced_url}")
148
  except Exception as e:
149
- logger.error(f"[Pipeline] Cloudinary upload failed: {e}")
150
 
151
- # ── Done ──────────────────────────────────────────────────────────────
152
  yield {
153
- "status": "done",
154
- "step": 4,
155
- "message": "Done!",
156
- "transcript": transcript,
157
- "translation": translation,
158
- "summary": summary,
159
  "enhancedAudio": enhanced_url,
160
  "stats": {
161
- "language": detected_lang.upper(),
162
- "noise_method": stats.get("noise_method", "Cleanvoice API"),
163
- "fillers_removed": stats.get("fillers_removed", 0),
164
- "stutters_removed": stats.get("stutters_removed", 0),
165
- "silences_removed_sec": stats.get("silences_removed_sec", 0),
166
- "breaths_reduced": stats.get("breaths_reduced", False),
167
- "mouth_sounds_removed": stats.get("mouth_sounds_removed", 0),
168
  "transcription_method": t_method,
169
- "translation_method": tl_method,
170
- "word_segments": len(word_segs),
171
- "transcript_words": len(transcript.split()),
172
  },
173
  }
174
 
175
  except Exception as e:
176
- logger.error(f"[Pipeline] Fatal error: {e}", exc_info=True)
177
- yield {"status": "error", "message": f"Error: {str(e)}"}
178
 
 
 
179
 
180
- # ══════════════════════════════════════════════════════════════════════════════
181
- # ROUTES
182
- # ══════════════════════════════════════════════════════════════════════════════
183
 
184
  @app.get("/api/health")
185
  async def health():
@@ -188,69 +157,59 @@ async def health():
188
 
189
  @app.post("/api/process-url")
190
  async def process_url(request: Request):
191
- data = await request.json()
192
- audio_url = data.get("audioUrl")
193
- audio_id = data.get("audioId", "")
194
- src_lang = data.get("srcLang", "auto")
195
- tgt_lang = data.get("tgtLang", "te")
196
- opt_fillers = data.get("optFillers", True)
197
  opt_stutters = data.get("optStutters", True)
198
  opt_silences = data.get("optSilences", True)
199
- opt_breaths = data.get("optBreaths", True)
200
- opt_mouth = data.get("optMouth", True)
201
 
202
  if not audio_url:
203
- return JSONResponse({"error": "audioUrl is required"}, status_code=400)
204
 
205
  async def generate():
206
- import sys
207
-
208
  def sse(obj):
209
- sys.stdout.flush()
210
  return "data: " + json.dumps(obj) + "\n\n"
211
 
212
  yield sse({"status": "processing", "step": 0, "message": "Downloading audio..."})
213
 
214
- # ── Download audio from URL ───────────────────────────────────────────
215
  try:
216
  resp = requests.get(audio_url, timeout=60, stream=True)
217
  resp.raise_for_status()
218
 
219
- # Detect extension β€” support WhatsApp .opus and common formats
220
  lower_url = audio_url.lower().split("?")[0]
221
- if ".opus" in lower_url: suffix = ".opus"
222
- elif ".ogg" in lower_url: suffix = ".ogg"
223
- elif ".aac" in lower_url: suffix = ".aac"
224
- elif ".m4a" in lower_url: suffix = ".m4a"
225
- elif ".wav" in lower_url: suffix = ".wav"
226
- else: suffix = ".mp3"
227
-
228
- tmp = tempfile.NamedTemporaryFile(delete=False, suffix=suffix)
229
- downloaded = 0
230
- total = int(resp.headers.get("content-length", 0))
231
  for chunk in resp.iter_content(chunk_size=65536):
232
  if chunk:
233
  tmp.write(chunk)
234
- downloaded += len(chunk)
235
- if total:
236
- pct = int(downloaded * 100 / total)
237
- yield sse({"status": "processing", "step": 0,
238
- "message": f"Downloading... {pct}%"})
239
  tmp.close()
 
240
  except Exception as e:
241
  yield sse({"status": "error", "message": f"Download failed: {e}"})
242
  return
243
 
244
- # ── Run pipeline ──────────────────────────────────────────────────────
245
- for result in run_pipeline(tmp.name, src_lang, tgt_lang,
246
- opt_fillers, opt_stutters, opt_silences,
247
- opt_breaths, opt_mouth):
 
248
  result["audioId"] = audio_id
249
  yield sse(result)
250
 
251
  try:
252
  os.unlink(tmp.name)
253
- except Exception:
254
  pass
255
 
256
  return StreamingResponse(
 
1
  """
2
  ClearWave AI β€” API Space (FastAPI)
3
  ===================================
 
4
 
5
  Pipeline:
6
+ 1. Download audio from URL
7
+ 2. Denoise β†’ Cleanvoice SDK (tested & working)
8
+ 3. Transcribe β†’ Groq Whisper large-v3 / faster-whisper fallback
9
+ 4. Translate β†’ Helsinki-NLP / NLLB / Google fallback
10
+ 5. Summarize β†’ Extractive (position-scored)
11
+ 6. Upload to Cloudinary
12
+
13
+ Environment vars: CLEANVOICE_API_KEY, CLOUD_NAME, API_KEY, API_SECRET, GROQ_API_KEY
 
14
  """
15
 
16
  import os
 
25
  from fastapi.responses import StreamingResponse, JSONResponse
26
  from fastapi.middleware.cors import CORSMiddleware
27
 
28
+ from denoiser import Denoiser # βœ… Your working denoiser
29
+ from transcriber import Transcriber # Your transcriber
30
+ from translator import Translator # Your translator
31
 
32
+ # ── Cloudinary config ─────────────────────────────────────────────
33
  cloudinary.config(
34
+ cloud_name=os.environ.get("CLOUD_NAME"),
35
+ api_key=os.environ.get("API_KEY"),
36
+ api_secret=os.environ.get("API_SECRET"),
37
  )
38
 
39
  logging.basicConfig(level=logging.INFO)
40
  logger = logging.getLogger(__name__)
41
 
42
+ # ── Singletons (loaded once at startup) ──────────────────────────
43
+ denoiser = Denoiser()
44
  transcriber = Transcriber()
45
+ translator = Translator()
46
 
47
  app = FastAPI(title="ClearWave AI API")
 
48
  app.add_middleware(
49
  CORSMiddleware,
50
  allow_origins=["*"],
 
53
  )
54
 
55
 
 
 
 
 
56
  def run_pipeline(audio_path, src_lang="auto", tgt_lang="te",
57
+ opt_fillers=True, opt_stutters=True, opt_silences=True):
 
58
  """
59
  Generator β€” yields SSE-style dicts at each step.
 
60
  """
61
+ out_dir = tempfile.mkdtemp()
 
62
  word_segs = []
63
+ stats = {}
64
 
65
  try:
66
+ # ── Step 1: Cleanvoice API ────────────────────────────────
67
  yield {"status": "processing", "step": 1,
68
  "message": "Step 1/4 β€” Enhancing audio with Cleanvoice..."}
69
+
70
+ result = denoiser.process(
71
+ audio_path, out_dir,
72
+ fillers=opt_fillers,
73
+ stutters=opt_stutters,
74
+ long_silences=opt_silences,
75
+ # βœ… breaths/mouth_sounds removed β€” handled by studio_sound=True
76
+ )
77
+ clean_audio = result["audio_path"]
78
+ stats = {
79
+ "noise_method": "Cleanvoice API",
80
+ "fillers": opt_fillers,
81
+ "stutters": opt_stutters,
82
+ "silences": opt_silences,
83
+ }
84
+ logger.info("Cleanvoice enhancement complete")
85
+
86
+ # ── Step 2: Transcribe ─────────────────────────────────────
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
87
  yield {"status": "processing", "step": 2,
88
  "message": "Step 2/4 β€” Transcribing..."}
89
+ transcript, detected_lang, t_method = transcriber.transcribe(clean_audio, src_lang)
90
  word_segs = transcriber._last_segments
91
 
92
+ # Clean transcript fillers too
93
  if opt_fillers:
94
  transcript = denoiser.clean_transcript_fillers(transcript)
95
 
96
+ logger.info(f"Transcription: {len(transcript.split())} words, lang={detected_lang}")
97
 
98
+ # ── Step 3: Translate ──────────────────────────────────────
99
  translation = transcript
100
+ tl_method = "same language"
101
  if tgt_lang != "auto" and detected_lang != tgt_lang:
102
  yield {"status": "processing", "step": 3,
103
  "message": "Step 3/4 β€” Translating..."}
104
  translation, tl_method = translator.translate(transcript, detected_lang, tgt_lang)
105
+ logger.info(f"Translation done via {tl_method}")
106
  else:
107
  yield {"status": "processing", "step": 3,
108
+ "message": "Step 3/4 β€” Skipping translation (same language)"}
109
 
110
+ # ── Step 4: Summarize & Cloudinary ────────────────────────
111
  yield {"status": "processing", "step": 4,
112
  "message": "Step 4/4 β€” Summarizing & uploading..."}
113
  summary = translator.summarize(transcript)
 
115
  enhanced_url = None
116
  try:
117
  upload_result = cloudinary.uploader.upload(
118
+ clean_audio,
119
+ resource_type="video", # audio files use "video"
120
  folder="clearwave_enhanced",
121
  )
122
  enhanced_url = upload_result["secure_url"]
123
+ logger.info(f"Cloudinary upload: {enhanced_url}")
124
  except Exception as e:
125
+ logger.error(f"Cloudinary failed: {e}")
126
 
 
127
  yield {
128
+ "status": "done",
129
+ "step": 4,
130
+ "message": "βœ… Complete!",
131
+ "transcript": transcript,
132
+ "translation": translation,
133
+ "summary": summary,
134
  "enhancedAudio": enhanced_url,
135
  "stats": {
136
+ "language": detected_lang.upper(),
137
+ "denoiser": stats,
 
 
 
 
 
138
  "transcription_method": t_method,
139
+ "translation_method": tl_method,
140
+ "word_segments": len(word_segs),
141
+ "transcript_words": len(transcript.split()),
142
  },
143
  }
144
 
145
  except Exception as e:
146
+ logger.error(f"Pipeline error: {e}", exc_info=True)
147
+ yield {"status": "error", "message": f"Pipeline failed: {str(e)}"}
148
 
149
+ # Cleanup
150
+ denoiser.cleanup_temp_files(out_dir)
151
 
 
 
 
152
 
153
  @app.get("/api/health")
154
  async def health():
 
157
 
158
  @app.post("/api/process-url")
159
  async def process_url(request: Request):
160
+ data = await request.json()
161
+ audio_url = data.get("audioUrl")
162
+ audio_id = data.get("audioId", "")
163
+ src_lang = data.get("srcLang", "auto")
164
+ tgt_lang = data.get("tgtLang", "te")
165
+ opt_fillers = data.get("optFillers", True)
166
  opt_stutters = data.get("optStutters", True)
167
  opt_silences = data.get("optSilences", True)
 
 
168
 
169
  if not audio_url:
170
+ return JSONResponse({"error": "audioUrl required"}, status_code=400)
171
 
172
  async def generate():
 
 
173
  def sse(obj):
 
174
  return "data: " + json.dumps(obj) + "\n\n"
175
 
176
  yield sse({"status": "processing", "step": 0, "message": "Downloading audio..."})
177
 
178
+ # Download audio from URL
179
  try:
180
  resp = requests.get(audio_url, timeout=60, stream=True)
181
  resp.raise_for_status()
182
 
183
+ # Detect format
184
  lower_url = audio_url.lower().split("?")[0]
185
+ suffix = ".mp3"
186
+ if ".opus" in lower_url: suffix = ".opus"
187
+ elif ".ogg" in lower_url: suffix = ".ogg"
188
+ elif ".aac" in lower_url: suffix = ".aac"
189
+ elif ".m4a" in lower_url: suffix = ".m4a"
190
+ elif ".wav" in lower_url: suffix = ".wav"
191
+
192
+ tmp = tempfile.NamedTemporaryFile(delete=False, suffix=suffix)
 
 
193
  for chunk in resp.iter_content(chunk_size=65536):
194
  if chunk:
195
  tmp.write(chunk)
 
 
 
 
 
196
  tmp.close()
197
+
198
  except Exception as e:
199
  yield sse({"status": "error", "message": f"Download failed: {e}"})
200
  return
201
 
202
+ # Run full pipeline
203
+ for result in run_pipeline(
204
+ tmp.name, src_lang, tgt_lang,
205
+ opt_fillers, opt_stutters, opt_silences
206
+ ):
207
  result["audioId"] = audio_id
208
  yield sse(result)
209
 
210
  try:
211
  os.unlink(tmp.name)
212
+ except:
213
  pass
214
 
215
  return StreamingResponse(