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

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +360 -17
app.py CHANGED
@@ -1,17 +1,360 @@
1
- ---
2
- title: SmartTranscribe — Rate-limited Urdu/English Summaries
3
- emoji: 🎙️
4
- colorFrom: indigo
5
- colorTo: purple
6
- sdk: gradio
7
- sdk_version: "4.44.0"
8
- app_file: app.py
9
- pinned: true
10
- ---
11
-
12
- # SmartTranscribe
13
-
14
- Notes:
15
- - This version respects Groq tokens-per-minute (TPM) limits by chunking transcripts and rate-limiting requests.
16
- - You can tweak TPM via env var `GROQ_TPM_LIMIT` (default 8000).
17
- - You can adjust chunk size via `CHUNK_CHARS` and output size via `OUT_TOKENS_PER_CHUNK`.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+