malleswar commited on
Commit
026fd11
Β·
verified Β·
1 Parent(s): 14a6ae4

Create app.py

Browse files
Files changed (1) hide show
  1. app.py +439 -0
app.py ADDED
@@ -0,0 +1,439 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import gradio as gr
2
+ import requests
3
+ import base64
4
+ import time
5
+ import json
6
+
7
+ # ============================================================================
8
+ # REPLACE THESE WITH YOUR MODAL URLS
9
+ # ============================================================================
10
+ MODAL_TRANSCRIBE_URL = "https://vnmalleswar-payyavula--subtitle-generator-test-process-video.modal.run"
11
+ MODAL_BURN_URL = "https://vnmalleswar-payyavula--subtitle-generator-test-burn-edit-099d8b.modal.run"
12
+
13
+ # ============================================================================
14
+ # STEP 1: TRANSCRIBE VIDEO (Calls Modal Backend)
15
+ # ============================================================================
16
+
17
+ def transcribe_video(video_file, language="hindi", progress=gr.Progress()):
18
+ """Call Modal backend to transcribe video"""
19
+
20
+ if not video_file:
21
+ return None, None, None, "❌ Please upload a video", ""
22
+
23
+ try:
24
+ progress(0.1, desc="Reading video file...")
25
+
26
+ # Read and encode video
27
+ with open(video_file, "rb") as f:
28
+ video_bytes = f.read()
29
+
30
+ video_base64 = base64.b64encode(video_bytes).decode('utf-8')
31
+
32
+ progress(0.2, desc="Uploading to Modal server...")
33
+
34
+ # Prepare request
35
+ payload = {
36
+ "video_base64": video_base64,
37
+ "language": language,
38
+ "position": "bottom-center"
39
+ }
40
+
41
+ progress(0.3, desc="Transcribing on GPU server (30-60 seconds)...")
42
+
43
+ # Call Modal transcribe endpoint
44
+ response = requests.post(
45
+ MODAL_TRANSCRIBE_URL,
46
+ json=payload,
47
+ timeout=600
48
+ )
49
+
50
+ if response.status_code != 200:
51
+ return None, None, None, f"❌ Server error: Status {response.status_code}", ""
52
+
53
+ result = response.json()
54
+
55
+ # Check for errors
56
+ if "error" in result:
57
+ error_msg = result["error"]
58
+ if "traceback" in result:
59
+ error_msg += f"\n\n{result['traceback']}"
60
+ return None, None, None, f"❌ Error: {error_msg}", ""
61
+
62
+ progress(0.9, desc="Processing results...")
63
+
64
+ # Get results
65
+ srt_content = result.get("srt_content", "")
66
+ video_base64_output = result.get("video_base64", "")
67
+ transcription = result.get("transcription", "")
68
+
69
+ # Save SRT file for download
70
+ timestamp = int(time.time())
71
+ srt_path = f"/tmp/subtitles_{timestamp}.srt"
72
+ with open(srt_path, "w", encoding="utf-8") as f:
73
+ f.write(srt_content)
74
+
75
+ # Save original video for editing workflow
76
+ original_video_path = f"/tmp/original_video_{timestamp}.mp4"
77
+ with open(video_file, "rb") as f:
78
+ original_bytes = f.read()
79
+ with open(original_video_path, "wb") as f:
80
+ f.write(original_bytes)
81
+
82
+ progress(1.0, desc="Complete!")
83
+
84
+ status = result.get("status", "Success")
85
+ status += f"\n\nπŸ“Š **Statistics:**"
86
+ status += f"\n- Original words: {result.get('original_word_count', 'N/A')}"
87
+ status += f"\n- Subtitle words: {result.get('subtitle_word_count', 'N/A')}"
88
+ status += f"\n- Words lost: {result.get('words_lost', 0)}"
89
+ status += f"\n- Device: {result.get('device_used', 'N/A')}"
90
+ status += f"\n- Model: {result.get('model_used', 'N/A')}"
91
+ status += f"\n\nπŸ“ **Transcription Preview:**\n{transcription[:300]}{'...' if len(transcription) > 300 else ''}"
92
+
93
+ return original_video_path, srt_content, srt_path, status, ""
94
+
95
+ except requests.Timeout:
96
+ return None, None, None, "❌ Timeout: Processing took too long. Try a shorter video.", ""
97
+
98
+ except Exception as e:
99
+ import traceback
100
+ error = f"❌ Error: {str(e)}\n\n{traceback.format_exc()}"
101
+ return None, None, None, error, ""
102
+
103
+ # ============================================================================
104
+ # STEP 2: BURN EDITED SUBTITLES (Calls Modal Backend)
105
+ # ============================================================================
106
+
107
+ def burn_edited_subtitles(original_video_path, edited_srt_content, progress=gr.Progress()):
108
+ """Call Modal backend to burn edited subtitles"""
109
+
110
+ if not original_video_path:
111
+ return None, "❌ No video found. Please transcribe a video first in Step 1."
112
+
113
+ if not edited_srt_content or not edited_srt_content.strip():
114
+ return None, "❌ No subtitles provided. Please edit subtitles first."
115
+
116
+ try:
117
+ progress(0.1, desc="Reading video file...")
118
+
119
+ # Read original video
120
+ with open(original_video_path, "rb") as f:
121
+ video_bytes = f.read()
122
+
123
+ video_base64 = base64.b64encode(video_bytes).decode('utf-8')
124
+
125
+ progress(0.2, desc="Parsing edited subtitles...")
126
+
127
+ # Parse edited SRT to subtitles array
128
+ subtitles = parse_srt_string(edited_srt_content)
129
+
130
+ if not subtitles:
131
+ return None, "❌ Could not parse subtitles. Please check the format."
132
+
133
+ progress(0.3, desc="Uploading to Modal server...")
134
+
135
+ # Prepare request
136
+ payload = {
137
+ "video_base64": video_base64,
138
+ "subtitles": subtitles,
139
+ "position": "bottom-center"
140
+ }
141
+
142
+ progress(0.4, desc="Burning subtitles on server (5-10 seconds)...")
143
+
144
+ # Call Modal burn endpoint
145
+ response = requests.post(
146
+ MODAL_BURN_URL,
147
+ json=payload,
148
+ timeout=300
149
+ )
150
+
151
+ if response.status_code != 200:
152
+ return None, f"❌ Server error: Status {response.status_code}"
153
+
154
+ result = response.json()
155
+
156
+ # Check for errors
157
+ if "error" in result:
158
+ error_msg = result["error"]
159
+ if "traceback" in result:
160
+ error_msg += f"\n\n{result['traceback']}"
161
+ return None, f"❌ Error: {error_msg}"
162
+
163
+ progress(0.8, desc="Downloading processed video...")
164
+
165
+ # Get output video
166
+ video_base64_output = result.get("video_base64", "")
167
+
168
+ # Save final video
169
+ timestamp = int(time.time())
170
+ final_video_path = f"/tmp/final_video_{timestamp}.mp4"
171
+ video_bytes = base64.b64decode(video_base64_output)
172
+ with open(final_video_path, "wb") as f:
173
+ f.write(video_bytes)
174
+
175
+ progress(1.0, desc="Complete!")
176
+
177
+ status = result.get("status", "Success")
178
+ status += f"\n\nβœ… Video ready! Burned {len(subtitles)} edited subtitles."
179
+
180
+ return final_video_path, status
181
+
182
+ except requests.Timeout:
183
+ return None, "❌ Timeout: Processing took too long."
184
+
185
+ except Exception as e:
186
+ import traceback
187
+ error = f"❌ Error: {str(e)}\n\n{traceback.format_exc()}"
188
+ return None, error
189
+
190
+ # ============================================================================
191
+ # HELPER FUNCTIONS
192
+ # ============================================================================
193
+
194
+ def parse_srt_string(srt_content):
195
+ """Parse SRT string to subtitle array for Modal API"""
196
+ subtitles = []
197
+ blocks = srt_content.strip().split('\n\n')
198
+
199
+ for block in blocks:
200
+ lines = block.strip().split('\n')
201
+ if len(lines) >= 3:
202
+ try:
203
+ index = int(lines[0])
204
+ times = lines[1].split(' --> ')
205
+ if len(times) == 2:
206
+ subtitles.append({
207
+ 'index': index,
208
+ 'start': times[0].strip(),
209
+ 'end': times[1].strip(),
210
+ 'text': '\n'.join(lines[2:])
211
+ })
212
+ except:
213
+ continue
214
+
215
+ return subtitles
216
+
217
+ # ============================================================================
218
+ # GRADIO INTERFACE
219
+ # ============================================================================
220
+
221
+ css = """
222
+ #subtitle_editor {
223
+ font-family: 'Courier New', monospace;
224
+ font-size: 14px;
225
+ }
226
+ .modal-info {
227
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
228
+ color: white;
229
+ padding: 20px;
230
+ border-radius: 10px;
231
+ margin: 15px 0;
232
+ box-shadow: 0 4px 6px rgba(0,0,0,0.1);
233
+ }
234
+ .step-box {
235
+ border: 2px solid #667eea;
236
+ border-radius: 10px;
237
+ padding: 20px;
238
+ margin: 10px 0;
239
+ }
240
+ """
241
+
242
+ with gr.Blocks(title="AI Subtitle Generator & Editor", css=css, theme=gr.themes.Soft()) as demo:
243
+
244
+ # Header
245
+ gr.Markdown(
246
+ """
247
+ # 🎬 AI Subtitle Generator & Editor
248
+
249
+ ### Powered by Whisper Large V3 Turbo on Modal GPU
250
+
251
+ **Two-Step Workflow:**
252
+ 1. πŸ“ **Transcribe** β†’ Upload video and generate subtitles automatically
253
+ 2. ✏️ **Edit & Burn** β†’ Modify subtitles and get final video (fast, no re-transcription!)
254
+ """
255
+ )
256
+
257
+ # Hidden states
258
+ stored_video = gr.State(None)
259
+ stored_subtitles_json = gr.State("")
260
+
261
+ gr.Markdown("---")
262
+
263
+ # ========================================================================
264
+ # STEP 1: TRANSCRIBE
265
+ # ========================================================================
266
+
267
+ with gr.Group(elem_classes="step-box"):
268
+ gr.Markdown("## πŸ“ Step 1: Upload & Transcribe Video")
269
+
270
+ with gr.Row():
271
+ with gr.Column(scale=1):
272
+ video_input = gr.Video(label="Upload Video File")
273
+ language_dropdown = gr.Dropdown(
274
+ choices=["hindi", "english", "auto"],
275
+ value="hindi",
276
+ label="Audio Language",
277
+ info="Select the language spoken in the video"
278
+ )
279
+ transcribe_btn = gr.Button("πŸŽ™οΈ Transcribe Video", variant="primary", size="lg")
280
+
281
+ with gr.Column(scale=1):
282
+ transcribe_status = gr.Textbox(
283
+ label="Transcription Status",
284
+ lines=12,
285
+ interactive=False
286
+ )
287
+ srt_download = gr.File(
288
+ label="πŸ“₯ Download SRT File",
289
+ interactive=False
290
+ )
291
+
292
+ gr.Markdown(
293
+ """
294
+ <div class="modal-info">
295
+ ⏱️ <b>Processing Time:</b> 30-60 seconds (depends on video length)<br>
296
+ πŸ–₯️ <b>Processing:</b> Modal GPU Server (T4)<br>
297
+ 🎯 <b>Model:</b> Whisper Large V3 Turbo
298
+ </div>
299
+ """
300
+ )
301
+
302
+ gr.Markdown("---")
303
+
304
+ # ========================================================================
305
+ # STEP 2: EDIT & BURN
306
+ # ========================================================================
307
+
308
+ with gr.Group(elem_classes="step-box"):
309
+ gr.Markdown("## ✏️ Step 2: Edit Subtitles & Burn to Video")
310
+ gr.Markdown(
311
+ """
312
+ **How to Edit:**
313
+ - Modify subtitle text directly in the box below
314
+ - Keep the SRT format: `Index` β†’ `Timestamp` β†’ `Text`
315
+ - Each subtitle separated by blank line
316
+ - Delete unwanted subtitle blocks entirely
317
+ """
318
+ )
319
+
320
+ with gr.Row():
321
+ with gr.Column(scale=1):
322
+ srt_editor = gr.Textbox(
323
+ label="✏️ Editable Subtitles (SRT Format)",
324
+ lines=20,
325
+ placeholder="Subtitles will appear here after transcription...\n\nFormat:\n1\n00:00:00,000 --> 00:00:02,000\nSubtitle text\n\n2\n00:00:02,000 --> 00:00:04,000\nNext subtitle...",
326
+ elem_id="subtitle_editor"
327
+ )
328
+
329
+ with gr.Accordion("πŸ’‘ Editing Tips", open=False):
330
+ gr.Markdown(
331
+ """
332
+ - **Change text:** Just modify the text lines
333
+ - **Delete subtitle:** Remove entire block (index + timestamp + text)
334
+ - **Adjust timing:** Edit timestamp line (format: `HH:MM:SS,mmm --> HH:MM:SS,mmm`)
335
+ - **Add subtitle:** Copy the format and add a new block
336
+ - **Re-index:** After deleting, you can renumber indices (1, 2, 3...)
337
+ """
338
+ )
339
+
340
+ with gr.Column(scale=1):
341
+ burn_btn = gr.Button("πŸ”₯ Burn Edited Subtitles to Video", variant="primary", size="lg")
342
+ burn_status = gr.Textbox(
343
+ label="Burn Status",
344
+ lines=5,
345
+ interactive=False
346
+ )
347
+ final_video = gr.Video(
348
+ label="πŸ“Ή Final Video with Edited Subtitles",
349
+ interactive=False
350
+ )
351
+
352
+ gr.Markdown(
353
+ """
354
+ <div class="modal-info">
355
+ ⏱️ <b>Processing Time:</b> 5-10 seconds (no re-transcription!)<br>
356
+ πŸ–₯️ <b>Processing:</b> Modal GPU Server<br>
357
+ ⚑ <b>Speed:</b> 5-10x faster than re-transcribing
358
+ </div>
359
+ """
360
+ )
361
+
362
+ gr.Markdown("---")
363
+
364
+ # Footer
365
+ with gr.Accordion("πŸ“š More Information", open=False):
366
+ gr.Markdown(
367
+ """
368
+ ### 🎯 Features:
369
+
370
+ - βœ… Automatic subtitle generation using Whisper AI
371
+ - βœ… Word-level timestamp accuracy
372
+ - βœ… Edit subtitles without re-transcription
373
+ - βœ… Support for Hindi, English, and auto-detection
374
+ - βœ… Customizable subtitle appearance
375
+ - βœ… Fast GPU processing on Modal
376
+
377
+ ### πŸ“Š Technical Details:
378
+
379
+ | Component | Details |
380
+ |-----------|---------|
381
+ | Backend | Modal Serverless |
382
+ | GPU | NVIDIA T4 |
383
+ | Model | Whisper Large V3 Turbo |
384
+ | Engine | faster-whisper |
385
+ | Font | Arial, size 20 |
386
+ | Position | Bottom center |
387
+ | Max Words | 3 per subtitle |
388
+
389
+ ### ⚑ Performance Comparison:
390
+
391
+ | Operation | Time |
392
+ |-----------|------|
393
+ | Step 1: Transcribe | 30-60 seconds |
394
+ | Step 2: Burn Only | 5-10 seconds |
395
+ | **Total (with edit)** | **35-70 seconds** |
396
+ | Traditional (re-transcribe) | 60-120 seconds |
397
+
398
+ ### πŸ”§ Setup:
399
+
400
+ This frontend connects to Modal backend endpoints. The backend handles all heavy processing.
401
+
402
+ ### πŸ› Troubleshooting:
403
+
404
+ - **"Server error"**: Check Modal deployment is active
405
+ - **"Timeout"**: Try a shorter video (under 5 minutes)
406
+ - **"Parse error"**: Check SRT format in editor
407
+ """
408
+ )
409
+
410
+ # ========================================================================
411
+ # EVENT HANDLERS
412
+ # ========================================================================
413
+
414
+ # Step 1: Transcribe
415
+ transcribe_btn.click(
416
+ fn=transcribe_video,
417
+ inputs=[video_input, language_dropdown],
418
+ outputs=[stored_video, srt_editor, srt_download, transcribe_status, stored_subtitles_json]
419
+ )
420
+
421
+ # Step 2: Burn edited subtitles
422
+ burn_btn.click(
423
+ fn=burn_edited_subtitles,
424
+ inputs=[stored_video, srt_editor],
425
+ outputs=[final_video, burn_status]
426
+ )
427
+
428
+ # ============================================================================
429
+ # LAUNCH
430
+ # ============================================================================
431
+
432
+ if __name__ == "__main__":
433
+ demo.queue()
434
+ demo.launch(
435
+ share=False,
436
+ show_error=True,
437
+ server_name="0.0.0.0",
438
+ server_port=7860
439
+ )