Turbiling commited on
Commit
b16b9a6
·
verified ·
1 Parent(s): 8a61c0b

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +103 -317
app.py CHANGED
@@ -1,360 +1,146 @@
1
- # app.py
2
  import os
3
- import time
4
- import math
5
- import shutil
6
- import tempfile
7
- import glob
8
- import subprocess
9
- from pathlib import Path
10
-
11
  import gradio as gr
 
12
  import yt_dlp
13
- import requests
14
  from pydub import AudioSegment
15
  from groq import Groq
16
 
17
- # -----------------------
18
- # Config / Env
19
- # -----------------------
20
  GROQ_API_KEY = os.getenv("GROQ_API_KEY")
21
- # TPM limit (tokens per minute) for your Groq org. Default 8000 (from your error).
22
- GROQ_TPM_LIMIT = int(os.getenv("GROQ_TPM_LIMIT", "8000"))
23
- # Desired output tokens per chunk when summarizing
24
- OUT_TOKENS_PER_CHUNK = int(os.getenv("OUT_TOKENS_PER_CHUNK", "120"))
25
- # Approx characters per token (conservative)
26
- CHARS_PER_TOKEN = int(os.getenv("CHARS_PER_TOKEN", "4"))
27
- # Chunk size in characters (input chunk) - conservative to keep tokens low
28
- CHUNK_CHARS = int(os.getenv("CHUNK_CHARS", "1800"))
29
-
30
  if not GROQ_API_KEY:
31
- raise EnvironmentError("Please set GROQ_API_KEY in the Space settings.")
32
 
33
  groq_client = Groq(api_key=GROQ_API_KEY)
34
 
35
- # -----------------------
36
- # Utilities
37
- # -----------------------
38
- def estimate_tokens(text: str) -> int:
39
- """Rough token estimate: characters / CHARS_PER_TOKEN."""
40
- if not text:
41
- return 1
42
- return max(1, math.ceil(len(text) / CHARS_PER_TOKEN))
43
-
44
- def rate_limit_sleep(input_tokens: int, output_tokens: int):
45
- """
46
- Sleep time to respect TPM limit.
47
- formula: sleep_seconds = (input+output)/TPM_LIMIT * 60
48
- This spaces requests so token-per-minute rate stays under limit.
49
- """
50
- tokens_needed = input_tokens + output_tokens
51
- if GROQ_TPM_LIMIT <= 0:
52
- return
53
- sleep_seconds = (tokens_needed / GROQ_TPM_LIMIT) * 60.0
54
- # enforce a small minimum to avoid too quick back-to-back calls
55
- if sleep_seconds < 0.5:
56
- sleep_seconds = 0.5
57
- time.sleep(sleep_seconds)
58
-
59
- # -----------------------
60
- # Audio / YouTube helpers
61
- # -----------------------
62
- def download_youtube_audio(youtube_url: str):
63
- tmpdir = tempfile.mkdtemp(prefix="yt_")
64
- outtmpl = os.path.join(tmpdir, "%(id)s.%(ext)s")
65
- ydl_opts = {
66
- "format": "bestaudio/best",
67
- "outtmpl": outtmpl,
68
- "quiet": True,
69
- "no_warnings": True,
70
- "postprocessors": [{
71
- "key": "FFmpegExtractAudio",
72
- "preferredcodec": "mp3",
73
- "preferredquality": "192",
74
- }],
75
- }
76
  try:
77
- with yt_dlp.YoutubeDL(ydl_opts) as ydl:
78
- info = ydl.extract_info(youtube_url, download=True)
79
- files = glob.glob(os.path.join(tmpdir, "*"))
80
- audio_path = None
81
- for f in files:
82
- if f.lower().endswith((".mp3", ".m4a", ".wav", ".webm", ".aac", ".ogg")):
83
- audio_path = f
84
- break
85
- if audio_path is None and files:
86
- audio_path = files[0]
87
- return audio_path, tmpdir
88
- except Exception as e:
89
- try:
90
- shutil.rmtree(tmpdir)
91
- except:
92
- pass
93
- return f"❌ Error downloading YouTube audio: {e}", None
94
-
95
- def convert_to_wav_safe(input_path: str):
96
- """
97
- Convert input file to 16kHz mono WAV, always produce a new temp file.
98
- Uses ffmpeg CLI and falls back to pydub if needed.
99
- """
100
- p = Path(input_path)
101
- tmpf = tempfile.NamedTemporaryFile(suffix=".wav", delete=False, prefix="conv_")
102
- tmpf.close()
103
- out_wav = tmpf.name
104
- try:
105
- # call ffmpeg
106
- res = subprocess.run(
107
  ["ffmpeg", "-y", "-i", str(input_path), "-ar", "16000", "-ac", "1", out_wav],
108
- stdout=subprocess.PIPE, stderr=subprocess.PIPE
 
109
  )
110
- if res.returncode != 0 or not Path(out_wav).exists():
111
- # fallback to pydub
112
  audio = AudioSegment.from_file(input_path)
113
  audio = audio.set_frame_rate(16000).set_channels(1)
114
  audio.export(out_wav, format="wav")
115
  return out_wav
116
  except Exception as e:
117
- # cleanup
118
- try:
119
- if Path(out_wav).exists():
120
- os.remove(out_wav)
121
- except:
122
- pass
123
- raise RuntimeError(f"Error converting to WAV: {e}")
124
-
125
- def split_text_to_chunks(text: str, chunk_chars: int = CHUNK_CHARS):
126
- """Split long text into character-based chunks (preserving word boundaries)."""
127
- words = text.split()
128
- chunks = []
129
- current = []
130
- current_len = 0
131
- for w in words:
132
- if current_len + len(w) + 1 > chunk_chars and current:
133
- chunks.append(" ".join(current))
134
- current = [w]
135
- current_len = len(w) + 1
136
- else:
137
- current.append(w)
138
- current_len += len(w) + 1
139
- if current:
140
- chunks.append(" ".join(current))
141
- return chunks
142
 
143
- # -----------------------
144
- # Transcription (chunked audio)
145
- # -----------------------
146
- def split_wav_to_chunks(wav_path: str, max_ms=5*60*1000):
147
- audio = AudioSegment.from_file(wav_path)
148
  chunks = []
149
- for i in range(0, len(audio), max_ms):
150
- chunk = audio[i:i+max_ms]
151
- tmpf = tempfile.NamedTemporaryFile(suffix=".wav", delete=False, prefix="chunk_")
152
- tmpf.close()
153
- chunk.export(tmpf.name, format="wav")
154
- chunks.append(tmpf.name)
155
  return chunks
156
 
157
- def transcribe_audio_with_groq(wav_or_audio_path: str):
158
- # convert first
159
- wav = convert_to_wav_safe(wav_or_audio_path)
160
  try:
161
- audio_chunks = split_wav_to_chunks(wav)
162
- pieces = []
163
- for chunk_path in audio_chunks:
164
- with open(chunk_path, "rb") as f:
165
- resp = groq_client.audio.transcriptions.create(model="whisper-large-v3", file=f)
166
- # flexibly extract text
167
- text_piece = ""
168
- if isinstance(resp, str):
169
- text_piece = resp
170
- elif hasattr(resp, "text"):
171
- text_piece = getattr(resp, "text")
172
- elif isinstance(resp, dict):
173
- text_piece = resp.get("text") or resp.get("transcription") or ""
174
- else:
175
- text_piece = str(resp)
176
- pieces.append(text_piece.strip())
177
- # cleanup chunk right away
178
- try:
179
- os.remove(chunk_path)
180
- except:
181
- pass
182
- aggregated = "\n".join([p for p in pieces if p])
183
- return aggregated
184
- finally:
185
- # cleanup wav
186
- try:
187
- if os.path.exists(wav):
188
- os.remove(wav)
189
- except:
190
- pass
191
 
192
- # -----------------------
193
- # Summarization (Urdu) with chunking + rate-limiting
194
- # -----------------------
195
- def summarise_in_urdu_with_groq(full_text: str):
196
- """
197
- 1) Split long transcript into manageable text chunks (CHUNK_CHARS).
198
- 2) For each chunk, call Groq LLM to produce a short Urdu summary (OUT_TOKENS_PER_CHUNK).
199
- 3) Rate-limit between calls based on estimated tokens so we don't exceed TPM.
200
- 4) Combine chunk summaries and ask Groq for a final condensed Urdu summary.
201
- """
202
- # 1) split text
203
- text_chunks = split_text_to_chunks(full_text, chunk_chars=CHUNK_CHARS)
204
- chunk_summaries = []
 
 
205
 
206
- for idx, chunk in enumerate(text_chunks, start=1):
207
- # build prompt in Urdu
 
208
  prompt = (
209
- "مندرجہ ذیل عبارت کا مختصر، صاف اور روان اردو خلاصہ (2-3 جملے) لکھیں:\n\n" + chunk
 
 
 
210
  )
211
- # estimate tokens
212
- input_tokens = estimate_tokens(chunk)
213
- output_tokens = OUT_TOKENS_PER_CHUNK
214
- # Call Groq chat completion
215
- try:
216
- resp = groq_client.chat.completions.create(
217
- model="openai/gpt-oss-120b",
218
- messages=[{"role": "user", "content": prompt}],
219
- temperature=0.3,
220
- max_tokens=output_tokens
221
- )
222
- # extract content
223
- summary_text = ""
224
- try:
225
- summary_text = resp.choices[0].message.content.strip()
226
- except Exception:
227
- # fallback to dict parsing
228
- if isinstance(resp, dict):
229
- # sometimes resp may contain 'choices' list etc.
230
- ch = resp.get("choices")
231
- if ch and isinstance(ch, list) and len(ch) > 0:
232
- summ = ch[0].get("message", {}).get("content") or ch[0].get("text") or ""
233
- summary_text = summ.strip() if summ else ""
234
- if not summary_text and isinstance(resp, str):
235
- summary_text = resp.strip()
236
- if not summary_text:
237
- summary_text = "[خلاصہ دستیاب نہیں — خالی نتیجہ]"
238
- chunk_summaries.append(summary_text)
239
- except Exception as e:
240
- return f"❌ Summarization failed while processing chunk {idx}: {e}"
241
-
242
- # 3) rate-limit sleep to respect TPM
243
- rate_limit_sleep(input_tokens, output_tokens)
244
-
245
- # 4) meta-summary: combine chunk summaries and condense
246
- combined = "\n".join(chunk_summaries)
247
- meta_prompt = (
248
- "مندرجہ ذیل مختصر خلاصوں کو ایک مربوط، جامع اور مختصر اردو خلاصہ میں تبدیل کریں (3-6 جملے):\n\n"
249
- + combined
250
- )
251
- input_tokens = estimate_tokens(combined)
252
- output_tokens = int(OUT_TOKENS_PER_CHUNK * 1.5) # allow slightly larger for meta summary
253
- try:
254
- resp2 = groq_client.chat.completions.create(
255
  model="openai/gpt-oss-120b",
256
- messages=[{"role": "user", "content": meta_prompt}],
257
- temperature=0.3,
258
- max_tokens=output_tokens
259
  )
260
- final_summary = ""
261
- try:
262
- final_summary = resp2.choices[0].message.content.strip()
263
- except Exception:
264
- if isinstance(resp2, dict):
265
- ch = resp2.get("choices")
266
- if ch and isinstance(ch, list) and len(ch) > 0:
267
- final_summary = ch[0].get("message", {}).get("content") or ch[0].get("text") or ""
268
- if not final_summary and isinstance(resp2, str):
269
- final_summary = resp2.strip()
270
- if not final_summary:
271
- final_summary = "[Meta-summary failed — empty result]"
272
  except Exception as e:
273
- return f"❌ Meta-summarization failed: {e}"
274
-
275
- # final rate-limit sleep
276
- rate_limit_sleep(input_tokens, output_tokens)
277
- return final_summary
278
 
279
- # -----------------------
280
- # Main pipeline: handles YouTube or uploaded file
281
- # -----------------------
282
- def process_input(youtube_url, uploaded_audio, summary_lang):
283
- tempdir = None
284
  try:
285
- if youtube_url and youtube_url.strip():
286
- audio_path, tempdir = download_youtube_audio(youtube_url.strip())
287
- if isinstance(audio_path, str) and audio_path.startswith("❌"):
288
- return audio_path, "", ""
289
- elif uploaded_audio:
290
- audio_path = uploaded_audio
 
291
  else:
292
- return "❌ Please upload an audio file or paste a YouTube link.", "", ""
293
 
294
- # Transcribe
295
- transcript = transcribe_audio_with_groq(audio_path)
296
  if transcript.startswith("❌"):
297
- return transcript, "", ""
 
 
 
298
 
299
- # Summarize: English or Urdu
300
- if summary_lang == "English":
301
- # Keep previous fast English summarization approach using Groq with a reasonable output size
302
- en_prompt = "Summarize the following text in clear English (3-6 sentences):\n\n" + transcript
303
- input_tokens = estimate_tokens(transcript)
304
- output_tokens = 180
305
- try:
306
- resp = groq_client.chat.completions.create(
307
- model="openai/gpt-oss-120b",
308
- messages=[{"role": "user", "content": en_prompt}],
309
- temperature=0.3,
310
- max_tokens=output_tokens
311
- )
312
- summary_text = ""
313
- try:
314
- summary_text = resp.choices[0].message.content.strip()
315
- except Exception:
316
- if isinstance(resp, dict):
317
- ch = resp.get("choices")
318
- if ch and isinstance(ch, list) and len(ch) > 0:
319
- summary_text = ch[0].get("message", {}).get("content") or ch[0].get("text") or ""
320
- if not summary_text and isinstance(resp, str):
321
- summary_text = resp.strip()
322
- if not summary_text:
323
- summary_text = "[Empty summary]"
324
- # rate-limit sleep
325
- rate_limit_sleep(input_tokens, output_tokens)
326
- return "✅ Done", transcript, summary_text
327
- except Exception as e:
328
- return f"❌ English summarization failed: {e}", transcript, ""
329
- else:
330
- # Urdu summarization via chunked approach
331
- ur_summary = summarise_in_urdu_with_groq(transcript)
332
- if ur_summary.startswith("❌"):
333
- return ur_summary, transcript, ""
334
- return "✅ Done", transcript, ur_summary
335
 
336
- finally:
337
- if tempdir:
338
- try:
339
- shutil.rmtree(tempdir)
340
- except:
341
- pass
342
 
343
- # -----------------------
344
- # Gradio UI
345
- # -----------------------
346
- with gr.Blocks(title="SmartTranscribe — Urdu/English (rate-limited summaries)") as demo:
347
- gr.Markdown("## SmartTranscribe — Upload audio or paste YouTube link. Choose summary language (English/Urdu).")
348
  with gr.Row():
349
- youtube_input = gr.Textbox(label="YouTube Link (optional)", placeholder="https://www.youtube.com/watch?v=...")
350
- summary_lang = gr.Dropdown(choices=["English", "Urdu"], value="English", label="Summary language")
351
- audio_input = gr.Audio(type="filepath", label="Upload or Record audio (optional)")
352
- process_btn = gr.Button("Transcribe & Summarize")
 
 
 
 
353
  status = gr.Textbox(label="Status")
354
- transcript_box = gr.Textbox(label="Transcription", lines=12)
355
- summary_box = gr.Textbox(label="Summary", lines=8)
356
- process_btn.click(fn=process_input, inputs=[youtube_input, audio_input, summary_lang], outputs=[status, transcript_box, summary_box])
357
 
358
- if __name__ == "__main__":
359
- demo.launch()
360
 
 
 
 
1
  import os
 
 
 
 
 
 
 
 
2
  import gradio as gr
3
+ import tempfile
4
  import yt_dlp
5
+ import subprocess
6
  from pydub import AudioSegment
7
  from groq import Groq
8
 
9
+ # ✅ Environment Variable Check
 
 
10
  GROQ_API_KEY = os.getenv("GROQ_API_KEY")
 
 
 
 
 
 
 
 
 
11
  if not GROQ_API_KEY:
12
+ raise EnvironmentError("Please set GROQ_API_KEY.")
13
 
14
  groq_client = Groq(api_key=GROQ_API_KEY)
15
 
16
+ # ✅ Convert audio to 16kHz mono WAV
17
+ def convert_to_wav(input_path):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
18
  try:
19
+ tmp_wav = tempfile.NamedTemporaryFile(suffix=".wav", delete=False, prefix="conv_")
20
+ out_wav = tmp_wav.name
21
+ result = subprocess.run(
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
22
  ["ffmpeg", "-y", "-i", str(input_path), "-ar", "16000", "-ac", "1", out_wav],
23
+ stdout=subprocess.PIPE,
24
+ stderr=subprocess.PIPE
25
  )
26
+ if result.returncode != 0:
 
27
  audio = AudioSegment.from_file(input_path)
28
  audio = audio.set_frame_rate(16000).set_channels(1)
29
  audio.export(out_wav, format="wav")
30
  return out_wav
31
  except Exception as e:
32
+ raise RuntimeError(f"❌ Error converting to WAV: {e}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
33
 
34
+ # ✅ Split long audio into 5-min chunks
35
+ def split_audio(file_path, max_duration_ms=5*60*1000):
36
+ audio = AudioSegment.from_file(file_path)
 
 
37
  chunks = []
38
+ for i in range(0, len(audio), max_duration_ms):
39
+ chunk = audio[i:i + max_duration_ms]
40
+ temp_chunk = tempfile.NamedTemporaryFile(suffix=".wav", delete=False)
41
+ chunk.export(temp_chunk.name, format="wav")
42
+ chunks.append(temp_chunk.name)
 
43
  return chunks
44
 
45
+ # ✅ Download YouTube Audio (with clear error messages)
46
+ def download_youtube_audio(youtube_url):
 
47
  try:
48
+ with tempfile.NamedTemporaryFile(suffix=".mp3", delete=False) as tmp_file:
49
+ ydl_opts = {
50
+ "format": "bestaudio/best",
51
+ "outtmpl": tmp_file.name,
52
+ "quiet": True,
53
+ "postprocessors": [{
54
+ "key": "FFmpegExtractAudio",
55
+ "preferredcodec": "mp3",
56
+ "preferredquality": "192",
57
+ }],
58
+ }
59
+ with yt_dlp.YoutubeDL(ydl_opts) as ydl:
60
+ ydl.download([youtube_url])
61
+ return tmp_file.name
62
+ except Exception as e:
63
+ raise RuntimeError(f"❌ Error downloading YouTube audio: {e}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
64
 
65
+ # ✅ Transcription using Groq Whisper
66
+ def transcribe_audio(audio_path):
67
+ try:
68
+ chunks = split_audio(audio_path)
69
+ transcript = ""
70
+ for chunk in chunks:
71
+ with open(chunk, "rb") as f:
72
+ response = groq_client.audio.transcriptions.create(
73
+ model="whisper-large-v3",
74
+ file=f
75
+ )
76
+ transcript += response.text + "\n"
77
+ return transcript.strip()
78
+ except Exception as e:
79
+ return f"❌ Error during transcription: {e}"
80
 
81
+ # Summarization with “Detailed Mode”
82
+ def summarize_text(text, lang):
83
+ try:
84
  prompt = (
85
+ f"Create a detailed, structured and comprehensive English summary of the following text. "
86
+ f"Cover key points, ideas, examples and conclusions clearly:\n\n{text}"
87
+ if lang == "English"
88
+ else f"مندرجہ ذیل عبارت کا تفصیلی، منظم اور جامع اردو خلاصہ تحریر کریں۔ خلاصے میں اہم نکات، مثالیں اور نتائج کو واضح طور پر بیان کریں:\n\n{text}"
89
  )
90
+ response = groq_client.chat.completions.create(
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
91
  model="openai/gpt-oss-120b",
92
+ messages=[{"role": "user", "content": prompt}],
93
+ temperature=0.6,
 
94
  )
95
+ return response.choices[0].message.content.strip()
 
 
 
 
 
 
 
 
 
 
 
96
  except Exception as e:
97
+ return f"❌ Summarization failed: {e}"
 
 
 
 
98
 
99
+ # ✅ Step 1: Transcription
100
+ def process_transcription(youtube_url, audio_file):
 
 
 
101
  try:
102
+ if youtube_url:
103
+ try:
104
+ audio_path = download_youtube_audio(youtube_url)
105
+ except Exception as e:
106
+ return f"❌ Error downloading YouTube audio: {e}", ""
107
+ elif audio_file:
108
+ audio_path = audio_file
109
  else:
110
+ return "❌ Please upload an audio or paste YouTube link.", ""
111
 
112
+ wav_path = convert_to_wav(audio_path)
113
+ transcript = transcribe_audio(wav_path)
114
  if transcript.startswith("❌"):
115
+ return transcript, ""
116
+ return "✅ Transcription Completed!", transcript
117
+ except Exception as e:
118
+ return f"❌ Error: {e}", ""
119
 
120
+ # ✅ Step 2: Generate Detailed Summary
121
+ def process_summary(transcript, lang):
122
+ if not transcript or transcript.startswith("❌"):
123
+ return " Please transcribe audio first."
124
+ return summarize_text(transcript, lang)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
125
 
126
+ # ✅ Gradio Interface
127
+ with gr.Blocks(title="🎧 Urdu & English Audio Transcriber + Summarizer") as app:
128
+ gr.Markdown("## 🎧 AI Audio & YouTube Transcriber — English & Urdu")
 
 
 
129
 
 
 
 
 
 
130
  with gr.Row():
131
+ youtube_link = gr.Textbox(label="📺 YouTube Link (optional)")
132
+ lang_choice = gr.Dropdown(["English", "Urdu"], value="English", label="🌐 Summary Language")
133
+
134
+ audio_input = gr.Audio(type="filepath", label="🎙️ Upload Audio (optional)")
135
+
136
+ transcribe_btn = gr.Button("📝 Step 1: Transcribe Audio / Video")
137
+ summarize_btn = gr.Button("🧩 Step 2: Generate Comprehensive Summary")
138
+
139
  status = gr.Textbox(label="Status")
140
+ transcript_box = gr.Textbox(label="📝 Transcription", lines=8)
141
+ summary_box = gr.Textbox(label="📘 Detailed Summary", lines=8)
 
142
 
143
+ transcribe_btn.click(process_transcription, [youtube_link, audio_input], [status, transcript_box])
144
+ summarize_btn.click(process_summary, [transcript_box, lang_choice], [summary_box])
145
 
146
+ app.launch()