malleswar commited on
Commit
944336f
Β·
verified Β·
1 Parent(s): 19c7c03

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +1071 -94
app.py CHANGED
@@ -2,17 +2,34 @@ import gradio as gr
2
  import requests
3
  import base64
4
  import time
5
- import os
 
6
 
7
- # Replace with your Modal URL from deployment
8
- MODAL_URL = "https://vnmalleswar-payyavula--subtitle-generator-process-video.modal.run"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9
 
10
- def process_video(video_file, language="hindi", subtitle_position="bottom-center", progress=gr.Progress()):
11
- """
12
- Process video by calling Modal backend
13
- """
14
  if not video_file:
15
- return None, None, "Please upload a video"
16
 
17
  try:
18
  progress(0.1, desc="Reading video file...")
@@ -23,26 +40,26 @@ def process_video(video_file, language="hindi", subtitle_position="bottom-center
23
 
24
  video_base64 = base64.b64encode(video_bytes).decode('utf-8')
25
 
26
- progress(0.2, desc="Uploading to processing server...")
27
 
28
  # Prepare request
29
  payload = {
30
  "video_base64": video_base64,
31
  "language": language,
32
- "position": subtitle_position
33
  }
34
 
35
- progress(0.3, desc="Processing on GPU server (this may take several minutes)...")
36
 
37
- # Call Modal backend with extended timeout
38
  response = requests.post(
39
- MODAL_URL,
40
  json=payload,
41
- timeout=600 # 10 minute timeout
42
  )
43
 
44
  if response.status_code != 200:
45
- return None, None, f"Error: Server returned status {response.status_code}"
46
 
47
  result = response.json()
48
 
@@ -50,109 +67,1069 @@ def process_video(video_file, language="hindi", subtitle_position="bottom-center
50
  if "error" in result:
51
  error_msg = result["error"]
52
  if "traceback" in result:
53
- error_msg += f"\n\nDetails:\n{result['traceback']}"
54
- return None, None, f"Processing failed: {error_msg}"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
55
 
56
  progress(0.8, desc="Downloading processed video...")
57
 
58
- # Decode results
59
- video_output_bytes = base64.b64decode(result["video_base64"])
60
- srt_content = result["srt_content"]
61
 
62
- # Save outputs to temp files
 
 
 
63
  timestamp = int(time.time())
64
- video_output_path = f"/tmp/output_video_{timestamp}.mp4"
65
- srt_output_path = f"/tmp/subtitles_{timestamp}.srt"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
66
 
67
- with open(video_output_path, "wb") as f:
68
- f.write(video_output_bytes)
 
69
 
70
- with open(srt_output_path, "w", encoding="utf-8") as f:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
71
  f.write(srt_content)
72
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
73
  progress(1.0, desc="Complete!")
74
 
75
- status_message = result["status"]
76
- if "transcription" in result:
77
- status_message += f"\n\n**Full Transcription:**\n{result['transcription']}"
 
 
 
 
 
 
 
78
 
79
- return srt_output_path, video_output_path, status_message
80
 
81
  except requests.Timeout:
82
- return None, None, "Error: Processing timed out after 10 minutes. Try a shorter video."
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
83
 
84
- except requests.RequestException as e:
85
- return None, None, f"Error: Network request failed - {str(e)}"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
86
 
87
  except Exception as e:
88
- return None, None, f"Error: {str(e)}"
89
-
90
-
91
- # Gradio Interface
92
- with gr.Blocks(theme=gr.themes.Soft(), title="Subtitle Generator") as demo:
93
-
94
- gr.Markdown("""
95
- # Subtitle Generator
96
- ### Powered by Whisper Large V3 on Modal GPU
97
-
98
- Upload a video and get AI-generated subtitles burned into the video.
99
- """)
100
-
101
- with gr.Tabs():
102
- with gr.Tab("Video Processing"):
103
- gr.Markdown("Upload video to get subtitles burned into the video")
104
-
105
- with gr.Row():
106
- with gr.Column():
107
- video_input = gr.Video(label="Upload Video")
108
- video_lang = gr.Dropdown(
109
- ["hindi", "english", "auto"],
110
- value="hindi",
111
- label="Language"
112
- )
113
- subtitle_pos = gr.Dropdown(
114
- [
115
- "top-left", "top-center", "top-right",
116
- "middle-left", "middle-center", "middle-right",
117
- "bottom-left", "bottom-center", "bottom-right"
118
- ],
119
- value="bottom-center",
120
- label="Subtitle Position"
121
- )
122
- video_btn = gr.Button("Generate Subtitles", variant="primary")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
123
 
124
- with gr.Column():
125
- status_output = gr.Textbox(label="Status", lines=10)
126
- srt_output = gr.File(label="Download SRT File")
127
- video_output = gr.Video(label="Final Video with Subtitles")
128
-
129
- video_btn.click(
130
- fn=process_video,
131
- inputs=[video_input, video_lang, subtitle_pos],
132
- outputs=[srt_output, video_output, status_output],
133
- api_name="process_video"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
134
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
135
 
136
- gr.Markdown("""
137
- ---
138
- ### How it works
139
 
140
- 1. Upload your video (MP4, AVI, MOV, etc.)
141
- 2. Select language (Hindi, English, or Auto-detect)
142
- 3. Choose subtitle position
143
- 4. Processing happens on GPU servers via Modal
144
- 5. Download your video with burned-in subtitles
 
145
 
146
- **Note:** Processing time depends on video length. Shorter videos (under 2 minutes) process faster.
 
 
 
 
 
147
 
148
- **Tip:** For best results, use videos with clear audio.
149
- """)
 
 
 
 
 
 
 
 
150
 
151
  if __name__ == "__main__":
152
- demo.queue(max_size=20).launch(
153
- server_name="0.0.0.0",
154
- server_port=7860,
155
  share=False,
156
- show_api=True,
157
- show_error=True
 
158
  )
 
2
  import requests
3
  import base64
4
  import time
5
+ import json
6
+ import re
7
 
8
+ # ============================================================================
9
+ # MODAL BACKEND URLS - UPDATED
10
+ # ============================================================================
11
+ MODAL_TRANSCRIBE_URL = "import gradio as gr
12
+ import requests
13
+ import base64
14
+ import time
15
+ import json
16
+ import re
17
+
18
+ # ============================================================================
19
+ # MODAL BACKEND URLS - UPDATED
20
+ # ============================================================================
21
+ MODAL_TRANSCRIBE_URL = "https://vnmalleswar-payyavula--subtitle-generator-test2-process-video.modal.run"
22
+ MODAL_BURN_URL = "https://vnmalleswar-payyavula--subtitle-generator-test2-burn-edi-ac46d8.modal.run"
23
+
24
+ # ============================================================================
25
+ # STEP 1: TRANSCRIBE VIDEO (Calls Modal Backend)
26
+ # ============================================================================
27
 
28
+ def transcribe_video(video_file, language="hindi", progress=gr.Progress()):
29
+ """Call Modal backend to transcribe video - Returns video WITH subtitles burned"""
30
+
 
31
  if not video_file:
32
+ return None, None, None, None, "❌ Please upload a video", ""
33
 
34
  try:
35
  progress(0.1, desc="Reading video file...")
 
40
 
41
  video_base64 = base64.b64encode(video_bytes).decode('utf-8')
42
 
43
+ progress(0.2, desc="Uploading to Modal server...")
44
 
45
  # Prepare request
46
  payload = {
47
  "video_base64": video_base64,
48
  "language": language,
49
+ "position": "bottom-center"
50
  }
51
 
52
+ progress(0.3, desc="Transcribing on GPU server (30-60 seconds)...")
53
 
54
+ # Call Modal transcribe endpoint
55
  response = requests.post(
56
+ MODAL_TRANSCRIBE_URL,
57
  json=payload,
58
+ timeout=600
59
  )
60
 
61
  if response.status_code != 200:
62
+ return None, None, None, None, f"❌ Server error: Status {response.status_code}\n{response.text}", ""
63
 
64
  result = response.json()
65
 
 
67
  if "error" in result:
68
  error_msg = result["error"]
69
  if "traceback" in result:
70
+ error_msg += f"\n\n{result['traceback']}"
71
+ return None, None, None, None, f"❌ Error: {error_msg}", ""
72
+
73
+ progress(0.9, desc="Processing results...")
74
+
75
+ # Get results
76
+ srt_content = result.get("srt_content", "")
77
+ video_base64_output = result.get("video_base64", "") # Video WITH subtitles
78
+ transcription = result.get("transcription", "")
79
+
80
+ # DEBUG: Print first few lines of SRT
81
+ print("=" * 60)
82
+ print("SRT CONTENT RECEIVED:")
83
+ print(srt_content[:500])
84
+ print("=" * 60)
85
+
86
+ # Save SRT file for download
87
+ timestamp = int(time.time())
88
+ srt_path = f"/tmp/subtitles_{timestamp}.srt"
89
+ with open(srt_path, "w", encoding="utf-8") as f:
90
+ f.write(srt_content)
91
+
92
+ # Save original video (WITHOUT subtitles) for editing workflow
93
+ original_video_path = f"/tmp/original_video_{timestamp}.mp4"
94
+ with open(video_file, "rb") as f:
95
+ original_bytes = f.read()
96
+ with open(original_video_path, "wb") as f:
97
+ f.write(original_bytes)
98
+
99
+ # Save video WITH burned subtitles for preview
100
+ preview_video_path = f"/tmp/preview_video_{timestamp}.mp4"
101
+ if video_base64_output:
102
+ preview_video_bytes = base64.b64decode(video_base64_output)
103
+ with open(preview_video_path, "wb") as f:
104
+ f.write(preview_video_bytes)
105
+ else:
106
+ preview_video_path = None
107
+
108
+ progress(1.0, desc="Complete!")
109
+
110
+ status = result.get("status", "Success")
111
+ status += f"\n\nπŸ“Š **Statistics:**"
112
+ status += f"\n- Original words: {result.get('original_word_count', 'N/A')}"
113
+ status += f"\n- Subtitle words: {result.get('subtitle_word_count', 'N/A')}"
114
+ status += f"\n- Words lost: {result.get('words_lost', 0)}"
115
+ status += f"\n- Device: {result.get('device_used', 'N/A')}"
116
+ status += f"\n- Model: {result.get('model_used', 'N/A')}"
117
+ status += f"\n\nπŸ“ **Transcription Preview:**\n{transcription[:300]}{'...' if len(transcription) > 300 else ''}"
118
+ status += f"\n\nβœ… **Video with subtitles is ready for preview!**"
119
+ status += f"\nπŸ’‘ You can edit subtitles below and re-burn if needed."
120
+
121
+ return original_video_path, preview_video_path, srt_content, srt_path, status, ""
122
+
123
+ except requests.Timeout:
124
+ return None, None, None, None, "❌ Timeout: Processing took too long. Try a shorter video.", ""
125
+
126
+ except Exception as e:
127
+ import traceback
128
+ error = f"❌ Error: {str(e)}\n\n{traceback.format_exc()}"
129
+ return None, None, None, None, error, ""
130
+
131
+ # ============================================================================
132
+ # STEP 2: BURN EDITED SUBTITLES (Calls Modal Backend)
133
+ # ============================================================================
134
+
135
+ def burn_edited_subtitles(original_video_path, edited_srt_content, progress=gr.Progress()):
136
+ """Call Modal backend to burn edited subtitles"""
137
+
138
+ if not original_video_path:
139
+ return None, "❌ No video found. Please transcribe a video first in Step 1."
140
+
141
+ if not edited_srt_content or not edited_srt_content.strip():
142
+ return None, "❌ No subtitles provided. Please edit subtitles first."
143
+
144
+ try:
145
+ progress(0.1, desc="Reading video file...")
146
+
147
+ # Read original video
148
+ with open(original_video_path, "rb") as f:
149
+ video_bytes = f.read()
150
+
151
+ video_base64 = base64.b64encode(video_bytes).decode('utf-8')
152
+
153
+ progress(0.2, desc="Parsing edited subtitles...")
154
+
155
+ # Parse edited SRT to subtitles array
156
+ subtitles = parse_srt_string(edited_srt_content)
157
+
158
+ print("=" * 60)
159
+ print(f"PARSED {len(subtitles)} SUBTITLES:")
160
+ for i, sub in enumerate(subtitles[:3]):
161
+ print(f"Subtitle {i+1}: {sub}")
162
+ print("=" * 60)
163
+
164
+ if not subtitles:
165
+ return None, "❌ Could not parse subtitles. Please check the format.\n\nMake sure each subtitle has:\n1. Index number\n2. Timestamp (HH:MM:SS,mmm --> HH:MM:SS,mmm)\n3. Text\n4. Blank line between subtitles"
166
+
167
+ progress(0.3, desc="Uploading to Modal server...")
168
+
169
+ # Prepare request
170
+ payload = {
171
+ "video_base64": video_base64,
172
+ "subtitles": subtitles,
173
+ "position": "bottom-center"
174
+ }
175
+
176
+ progress(0.4, desc="Burning subtitles on server (5-10 seconds)...")
177
+
178
+ print(f"Sending {len(subtitles)} subtitles to Modal...")
179
+
180
+ # Call Modal burn endpoint
181
+ response = requests.post(
182
+ MODAL_BURN_URL,
183
+ json=payload,
184
+ timeout=300
185
+ )
186
+
187
+ print(f"Response status: {response.status_code}")
188
+
189
+ if response.status_code != 200:
190
+ return None, f"❌ Server error: Status {response.status_code}\n{response.text}"
191
+
192
+ result = response.json()
193
+
194
+ # Check for errors
195
+ if "error" in result:
196
+ error_msg = result["error"]
197
+ if "traceback" in result:
198
+ error_msg += f"\n\n{result['traceback']}"
199
+ return None, f"❌ Error: {error_msg}"
200
 
201
  progress(0.8, desc="Downloading processed video...")
202
 
203
+ # Get output video
204
+ video_base64_output = result.get("video_base64", "")
 
205
 
206
+ if not video_base64_output:
207
+ return None, "❌ No video returned from server"
208
+
209
+ # Save final video
210
  timestamp = int(time.time())
211
+ final_video_path = f"/tmp/final_video_{timestamp}.mp4"
212
+ video_bytes = base64.b64decode(video_base64_output)
213
+ with open(final_video_path, "wb") as f:
214
+ f.write(video_bytes)
215
+
216
+ print(f"Saved final video to {final_video_path}")
217
+
218
+ progress(1.0, desc="Complete!")
219
+
220
+ status = result.get("status", "Success")
221
+ status += f"\n\nβœ… Video ready! Burned {len(subtitles)} edited subtitles."
222
+ status += f"\n\nπŸ” **Debug Info:**"
223
+ status += f"\n- Subtitles sent: {len(subtitles)}"
224
+ status += f"\n- Server response: {result.get('subtitle_count', 'N/A')}"
225
+
226
+ return final_video_path, status
227
+
228
+ except requests.Timeout:
229
+ return None, "❌ Timeout: Processing took too long."
230
+
231
+ except Exception as e:
232
+ import traceback
233
+ error = f"❌ Error: {str(e)}\n\n{traceback.format_exc()}"
234
+ return None, error
235
+
236
+ # ============================================================================
237
+ # HELPER FUNCTIONS - IMPROVED SRT PARSING
238
+ # ============================================================================
239
+
240
+ def parse_srt_string(srt_content):
241
+ """Parse SRT string to subtitle array for Modal API - ROBUST VERSION"""
242
+ subtitles = []
243
+
244
+ # Split by double newline OR by subtitle index pattern
245
+ blocks = re.split(r'\n\s*\n', srt_content.strip())
246
+
247
+ for block in blocks:
248
+ block = block.strip()
249
+ if not block:
250
+ continue
251
+
252
+ lines = block.split('\n')
253
 
254
+ # Need at least 3 lines: index, timestamp, text
255
+ if len(lines) < 3:
256
+ continue
257
 
258
+ try:
259
+ # Line 1: Index (must be a number)
260
+ index_line = lines[0].strip()
261
+
262
+ # Try to extract number from line
263
+ index_match = re.search(r'(\d+)', index_line)
264
+ if not index_match:
265
+ continue
266
+
267
+ index = int(index_match.group(1))
268
+
269
+ # Line 2: Timestamp
270
+ timestamp_line = lines[1].strip()
271
+
272
+ # Parse timestamp: HH:MM:SS,mmm --> HH:MM:SS,mmm
273
+ timestamp_match = re.search(
274
+ r'(\d{2}:\d{2}:\d{2},\d{3})\s*-->\s*(\d{2}:\d{2}:\d{2},\d{3})',
275
+ timestamp_line
276
+ )
277
+
278
+ if not timestamp_match:
279
+ print(f"⚠️ Could not parse timestamp: {timestamp_line}")
280
+ continue
281
+
282
+ start_time = timestamp_match.group(1)
283
+ end_time = timestamp_match.group(2)
284
+
285
+ # Lines 3+: Text (can be multiple lines)
286
+ text = '\n'.join(lines[2:]).strip()
287
+
288
+ if not text:
289
+ print(f"⚠️ Empty text for subtitle {index}")
290
+ continue
291
+
292
+ subtitles.append({
293
+ 'index': index,
294
+ 'start': start_time,
295
+ 'end': end_time,
296
+ 'text': text
297
+ })
298
+
299
+ except Exception as e:
300
+ print(f"⚠️ Error parsing block: {e}")
301
+ print(f"Block content: {block[:100]}")
302
+ continue
303
+
304
+ return subtitles
305
+
306
+ # ============================================================================
307
+ # TEST PARSING FUNCTION
308
+ # ============================================================================
309
+
310
+ def test_srt_parsing(srt_content):
311
+ """Test function to debug SRT parsing"""
312
+ if not srt_content or not srt_content.strip():
313
+ return "❌ No content to parse"
314
+
315
+ subtitles = parse_srt_string(srt_content)
316
+
317
+ result = f"βœ… Parsed {len(subtitles)} subtitles\n\n"
318
+
319
+ if len(subtitles) > 0:
320
+ result += "πŸ“ First 3 subtitles:\n\n"
321
+ for i, sub in enumerate(subtitles[:3]):
322
+ result += f"**Subtitle {sub['index']}:**\n"
323
+ result += f"Time: {sub['start']} --> {sub['end']}\n"
324
+ result += f"Text: {sub['text']}\n\n"
325
+ else:
326
+ result += "❌ No subtitles could be parsed!\n\n"
327
+ result += "**Debug Info:**\n"
328
+ result += f"Content length: {len(srt_content)} chars\n"
329
+ result += f"First 200 chars:\n{srt_content[:200]}\n"
330
+
331
+ return result
332
+
333
+ # ============================================================================
334
+ # GRADIO INTERFACE
335
+ # ============================================================================
336
+
337
+ css = """
338
+ #subtitle_editor {
339
+ font-family: 'Courier New', monospace;
340
+ font-size: 14px;
341
+ }
342
+ .modal-info {
343
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
344
+ color: white;
345
+ padding: 20px;
346
+ border-radius: 10px;
347
+ margin: 15px 0;
348
+ box-shadow: 0 4px 6px rgba(0,0,0,0.1);
349
+ }
350
+ .step-box {
351
+ border: 2px solid #667eea;
352
+ border-radius: 10px;
353
+ padding: 20px;
354
+ margin: 10px 0;
355
+ }
356
+ """
357
+
358
+ with gr.Blocks(title="AI Subtitle Generator & Editor", css=css, theme=gr.themes.Soft()) as demo:
359
+
360
+ # Header
361
+ gr.Markdown(
362
+ """
363
+ # 🎬 AI Subtitle Generator & Editor
364
+
365
+ ### Powered by Whisper Large V3 Turbo on Modal GPU
366
+
367
+ **Two-Step Workflow:**
368
+ 1. πŸ“ **Transcribe** β†’ Upload video, get auto-generated subtitles + preview video
369
+ 2. ✏️ **Edit & Re-burn** (Optional) β†’ Modify subtitles and get updated video
370
+ """
371
+ )
372
+
373
+ # Hidden states
374
+ stored_video = gr.State(None)
375
+ stored_subtitles_json = gr.State("")
376
+
377
+ gr.Markdown("---")
378
+
379
+ # ========================================================================
380
+ # STEP 1: TRANSCRIBE
381
+ # ========================================================================
382
+
383
+ with gr.Group(elem_classes="step-box"):
384
+ gr.Markdown("## πŸ“ Step 1: Upload & Transcribe Video")
385
+ gr.Markdown("*This will automatically generate subtitles AND burn them into the video for preview*")
386
+
387
+ with gr.Row():
388
+ with gr.Column(scale=1):
389
+ video_input = gr.Video(label="Upload Video File")
390
+ language_dropdown = gr.Dropdown(
391
+ choices=["hindi", "english", "auto"],
392
+ value="hindi",
393
+ label="Audio Language",
394
+ info="Select the language spoken in the video"
395
+ )
396
+ transcribe_btn = gr.Button("πŸŽ™οΈ Transcribe & Generate Subtitles", variant="primary", size="lg")
397
+
398
+ with gr.Column(scale=1):
399
+ transcribe_status = gr.Textbox(
400
+ label="Transcription Status",
401
+ lines=15,
402
+ interactive=False
403
+ )
404
+ srt_download = gr.File(
405
+ label="πŸ“₯ Download SRT File",
406
+ interactive=False
407
+ )
408
+
409
+ # Preview video with subtitles
410
+ with gr.Row():
411
+ preview_video = gr.Video(
412
+ label="πŸ“Ή Preview: Video with Generated Subtitles",
413
+ interactive=False
414
+ )
415
+
416
+ gr.Markdown(
417
+ """
418
+ <div class="modal-info">
419
+ ⏱️ <b>Processing Time:</b> 30-60 seconds<br>
420
+ πŸ–₯️ <b>Processing:</b> Modal GPU Server (T4)<br>
421
+ ✨ <b>Output:</b> Video with burned subtitles + Editable SRT file
422
+ </div>
423
+ """
424
+ )
425
+
426
+ gr.Markdown("---")
427
+
428
+ # ========================================================================
429
+ # STEP 2: EDIT & RE-BURN (OPTIONAL)
430
+ # ========================================================================
431
+
432
+ with gr.Group(elem_classes="step-box"):
433
+ gr.Markdown("## ✏️ Step 2: Edit Subtitles & Re-burn (Optional)")
434
+ gr.Markdown("*If you're happy with the preview above, you're done! Otherwise, edit subtitles below and re-burn.*")
435
+
436
+ with gr.Row():
437
+ with gr.Column(scale=1):
438
+ srt_editor = gr.Textbox(
439
+ label="✏️ Editable Subtitles (SRT Format)",
440
+ lines=20,
441
+ placeholder="Subtitles will appear here after transcription...\n\nYou can edit the text, timing, or delete entries.\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...",
442
+ elem_id="subtitle_editor"
443
+ )
444
+
445
+ with gr.Row():
446
+ test_parse_btn = gr.Button("πŸ§ͺ Test Parsing", size="sm")
447
+ burn_btn = gr.Button("πŸ”₯ Re-burn with Edited Subtitles", variant="primary", size="lg", scale=2)
448
+
449
+ with gr.Column(scale=1):
450
+ parse_test_output = gr.Textbox(
451
+ label="πŸ§ͺ Parse Test Results",
452
+ lines=10,
453
+ interactive=False,
454
+ visible=True
455
+ )
456
+ burn_status = gr.Textbox(
457
+ label="Re-burn Status",
458
+ lines=6,
459
+ interactive=False
460
+ )
461
+ final_video = gr.Video(
462
+ label="πŸ“Ή Final Video with Edited Subtitles",
463
+ interactive=False
464
+ )
465
+
466
+ with gr.Accordion("πŸ’‘ Editing Tips", open=False):
467
+ gr.Markdown(
468
+ """
469
+ **How to edit subtitles:**
470
+ - ✏️ **Change text:** Just modify the text lines
471
+ - πŸ—‘οΈ **Delete subtitle:** Remove entire block (index + timestamp + text)
472
+ - ⏰ **Adjust timing:** Edit timestamp line (keep format: `HH:MM:SS,mmm --> HH:MM:SS,mmm`)
473
+ - βž• **Add subtitle:** Copy format and add new block
474
+ - πŸ”’ **Re-index:** After deleting, you can renumber indices (1, 2, 3...)
475
+
476
+ **Format rules:**
477
+ ```
478
+ 1
479
+ 00:00:00,000 --> 00:00:02,000
480
+ Your subtitle text here
481
+
482
+ 2
483
+ 00:00:02,000 --> 00:00:04,000
484
+ Next subtitle text
485
+ ```
486
+
487
+ πŸ’‘ **Use "Test Parsing" button to verify your edits before re-burning!**
488
+ """
489
+ )
490
+
491
+ gr.Markdown(
492
+ """
493
+ <div class="modal-info">
494
+ ⏱️ <b>Processing Time:</b> 5-10 seconds (no re-transcription!)<br>
495
+ πŸ–₯️ <b>Processing:</b> Modal GPU Server<br>
496
+ ⚑ <b>Speed:</b> Much faster than re-transcribing
497
+ </div>
498
+ """
499
+ )
500
+
501
+ gr.Markdown("---")
502
+
503
+ # Footer
504
+ with gr.Accordion("πŸ“š More Information", open=False):
505
+ gr.Markdown(
506
+ f"""
507
+ ### 🎯 Features:
508
+
509
+ - βœ… Automatic subtitle generation using Whisper AI
510
+ - βœ… Word-level timestamp accuracy
511
+ - βœ… Instant video preview with subtitles
512
+ - βœ… Edit subtitles without re-transcription
513
+ - βœ… Support for Hindi, English, and auto-detection
514
+ - βœ… Fast GPU processing on Modal
515
+
516
+ ### πŸ“Š Technical Details:
517
+
518
+ | Component | Details |
519
+ |-----------|---------|
520
+ | Backend | Modal Serverless |
521
+ | GPU | NVIDIA T4 |
522
+ | Model | Whisper Large V3 Turbo |
523
+ | Engine | faster-whisper |
524
+ | Font | Arial, size 20 |
525
+ | Position | Bottom center |
526
+ | Max Words | 3 per subtitle |
527
+
528
+ ### ⚑ Performance:
529
+
530
+ | Operation | Time |
531
+ |-----------|------|
532
+ | Transcribe + Burn | 30-60 seconds |
533
+ | Edit + Re-burn | 5-10 seconds |
534
+
535
+ ### πŸ”§ Backend URLs:
536
+
537
+ - Transcribe: `{MODAL_TRANSCRIBE_URL}`
538
+ - Burn: `{MODAL_BURN_URL}`
539
+ """
540
+ )
541
+
542
+ # ========================================================================
543
+ # EVENT HANDLERS
544
+ # ========================================================================
545
+
546
+ # Step 1: Transcribe (returns video WITH subtitles)
547
+ transcribe_btn.click(
548
+ fn=transcribe_video,
549
+ inputs=[video_input, language_dropdown],
550
+ outputs=[stored_video, preview_video, srt_editor, srt_download, transcribe_status, stored_subtitles_json]
551
+ )
552
+
553
+ # Test SRT parsing
554
+ test_parse_btn.click(
555
+ fn=test_srt_parsing,
556
+ inputs=[srt_editor],
557
+ outputs=[parse_test_output]
558
+ )
559
+
560
+ # Step 2: Re-burn edited subtitles
561
+ burn_btn.click(
562
+ fn=burn_edited_subtitles,
563
+ inputs=[stored_video, srt_editor],
564
+ outputs=[final_video, burn_status]
565
+ )
566
+
567
+ # ============================================================================
568
+ # LAUNCH
569
+ # ============================================================================
570
+
571
+ if __name__ == "__main__":
572
+ demo.queue()
573
+ demo.launch(
574
+ share=False,
575
+ show_error=True,
576
+ server_name="0.0.0.0",
577
+ server_port=7860
578
+ )"
579
+ MODAL_BURN_URL = "https://vnmalleswar-payyavula--subtitle-generator-test-burn-edit-099d8b.modal.run"
580
+
581
+ # ============================================================================
582
+ # STEP 1: TRANSCRIBE VIDEO (Calls Modal Backend)
583
+ # ============================================================================
584
+
585
+ def transcribe_video(video_file, language="hindi", progress=gr.Progress()):
586
+ """Call Modal backend to transcribe video - Returns video WITH subtitles burned"""
587
+
588
+ if not video_file:
589
+ return None, None, None, None, "❌ Please upload a video", ""
590
+
591
+ try:
592
+ progress(0.1, desc="Reading video file...")
593
+
594
+ # Read and encode video
595
+ with open(video_file, "rb") as f:
596
+ video_bytes = f.read()
597
+
598
+ video_base64 = base64.b64encode(video_bytes).decode('utf-8')
599
+
600
+ progress(0.2, desc="Uploading to Modal server...")
601
+
602
+ # Prepare request
603
+ payload = {
604
+ "video_base64": video_base64,
605
+ "language": language,
606
+ "position": "bottom-center"
607
+ }
608
+
609
+ progress(0.3, desc="Transcribing on GPU server (30-60 seconds)...")
610
+
611
+ # Call Modal transcribe endpoint
612
+ response = requests.post(
613
+ MODAL_TRANSCRIBE_URL,
614
+ json=payload,
615
+ timeout=600
616
+ )
617
+
618
+ if response.status_code != 200:
619
+ return None, None, None, None, f"❌ Server error: Status {response.status_code}\n{response.text}", ""
620
+
621
+ result = response.json()
622
+
623
+ # Check for errors
624
+ if "error" in result:
625
+ error_msg = result["error"]
626
+ if "traceback" in result:
627
+ error_msg += f"\n\n{result['traceback']}"
628
+ return None, None, None, None, f"❌ Error: {error_msg}", ""
629
+
630
+ progress(0.9, desc="Processing results...")
631
+
632
+ # Get results
633
+ srt_content = result.get("srt_content", "")
634
+ video_base64_output = result.get("video_base64", "") # Video WITH subtitles
635
+ transcription = result.get("transcription", "")
636
+
637
+ # DEBUG: Print first few lines of SRT
638
+ print("=" * 60)
639
+ print("SRT CONTENT RECEIVED:")
640
+ print(srt_content[:500])
641
+ print("=" * 60)
642
+
643
+ # Save SRT file for download
644
+ timestamp = int(time.time())
645
+ srt_path = f"/tmp/subtitles_{timestamp}.srt"
646
+ with open(srt_path, "w", encoding="utf-8") as f:
647
  f.write(srt_content)
648
 
649
+ # Save original video (WITHOUT subtitles) for editing workflow
650
+ original_video_path = f"/tmp/original_video_{timestamp}.mp4"
651
+ with open(video_file, "rb") as f:
652
+ original_bytes = f.read()
653
+ with open(original_video_path, "wb") as f:
654
+ f.write(original_bytes)
655
+
656
+ # Save video WITH burned subtitles for preview
657
+ preview_video_path = f"/tmp/preview_video_{timestamp}.mp4"
658
+ if video_base64_output:
659
+ preview_video_bytes = base64.b64decode(video_base64_output)
660
+ with open(preview_video_path, "wb") as f:
661
+ f.write(preview_video_bytes)
662
+ else:
663
+ preview_video_path = None
664
+
665
  progress(1.0, desc="Complete!")
666
 
667
+ status = result.get("status", "Success")
668
+ status += f"\n\nπŸ“Š **Statistics:**"
669
+ status += f"\n- Original words: {result.get('original_word_count', 'N/A')}"
670
+ status += f"\n- Subtitle words: {result.get('subtitle_word_count', 'N/A')}"
671
+ status += f"\n- Words lost: {result.get('words_lost', 0)}"
672
+ status += f"\n- Device: {result.get('device_used', 'N/A')}"
673
+ status += f"\n- Model: {result.get('model_used', 'N/A')}"
674
+ status += f"\n\nπŸ“ **Transcription Preview:**\n{transcription[:300]}{'...' if len(transcription) > 300 else ''}"
675
+ status += f"\n\nβœ… **Video with subtitles is ready for preview!**"
676
+ status += f"\nπŸ’‘ You can edit subtitles below and re-burn if needed."
677
 
678
+ return original_video_path, preview_video_path, srt_content, srt_path, status, ""
679
 
680
  except requests.Timeout:
681
+ return None, None, None, None, "❌ Timeout: Processing took too long. Try a shorter video.", ""
682
+
683
+ except Exception as e:
684
+ import traceback
685
+ error = f"❌ Error: {str(e)}\n\n{traceback.format_exc()}"
686
+ return None, None, None, None, error, ""
687
+
688
+ # ============================================================================
689
+ # STEP 2: BURN EDITED SUBTITLES (Calls Modal Backend)
690
+ # ============================================================================
691
+
692
+ def burn_edited_subtitles(original_video_path, edited_srt_content, progress=gr.Progress()):
693
+ """Call Modal backend to burn edited subtitles"""
694
+
695
+ if not original_video_path:
696
+ return None, "❌ No video found. Please transcribe a video first in Step 1."
697
 
698
+ if not edited_srt_content or not edited_srt_content.strip():
699
+ return None, "❌ No subtitles provided. Please edit subtitles first."
700
+
701
+ try:
702
+ progress(0.1, desc="Reading video file...")
703
+
704
+ # Read original video
705
+ with open(original_video_path, "rb") as f:
706
+ video_bytes = f.read()
707
+
708
+ video_base64 = base64.b64encode(video_bytes).decode('utf-8')
709
+
710
+ progress(0.2, desc="Parsing edited subtitles...")
711
+
712
+ # Parse edited SRT to subtitles array
713
+ subtitles = parse_srt_string(edited_srt_content)
714
+
715
+ print("=" * 60)
716
+ print(f"PARSED {len(subtitles)} SUBTITLES:")
717
+ for i, sub in enumerate(subtitles[:3]):
718
+ print(f"Subtitle {i+1}: {sub}")
719
+ print("=" * 60)
720
+
721
+ if not subtitles:
722
+ return None, "❌ Could not parse subtitles. Please check the format.\n\nMake sure each subtitle has:\n1. Index number\n2. Timestamp (HH:MM:SS,mmm --> HH:MM:SS,mmm)\n3. Text\n4. Blank line between subtitles"
723
+
724
+ progress(0.3, desc="Uploading to Modal server...")
725
+
726
+ # Prepare request
727
+ payload = {
728
+ "video_base64": video_base64,
729
+ "subtitles": subtitles,
730
+ "position": "bottom-center"
731
+ }
732
+
733
+ progress(0.4, desc="Burning subtitles on server (5-10 seconds)...")
734
+
735
+ print(f"Sending {len(subtitles)} subtitles to Modal...")
736
+
737
+ # Call Modal burn endpoint
738
+ response = requests.post(
739
+ MODAL_BURN_URL,
740
+ json=payload,
741
+ timeout=300
742
+ )
743
+
744
+ print(f"Response status: {response.status_code}")
745
+
746
+ if response.status_code != 200:
747
+ return None, f"❌ Server error: Status {response.status_code}\n{response.text}"
748
+
749
+ result = response.json()
750
+
751
+ # Check for errors
752
+ if "error" in result:
753
+ error_msg = result["error"]
754
+ if "traceback" in result:
755
+ error_msg += f"\n\n{result['traceback']}"
756
+ return None, f"❌ Error: {error_msg}"
757
+
758
+ progress(0.8, desc="Downloading processed video...")
759
+
760
+ # Get output video
761
+ video_base64_output = result.get("video_base64", "")
762
+
763
+ if not video_base64_output:
764
+ return None, "❌ No video returned from server"
765
+
766
+ # Save final video
767
+ timestamp = int(time.time())
768
+ final_video_path = f"/tmp/final_video_{timestamp}.mp4"
769
+ video_bytes = base64.b64decode(video_base64_output)
770
+ with open(final_video_path, "wb") as f:
771
+ f.write(video_bytes)
772
+
773
+ print(f"Saved final video to {final_video_path}")
774
+
775
+ progress(1.0, desc="Complete!")
776
+
777
+ status = result.get("status", "Success")
778
+ status += f"\n\nβœ… Video ready! Burned {len(subtitles)} edited subtitles."
779
+ status += f"\n\nπŸ” **Debug Info:**"
780
+ status += f"\n- Subtitles sent: {len(subtitles)}"
781
+ status += f"\n- Server response: {result.get('subtitle_count', 'N/A')}"
782
+
783
+ return final_video_path, status
784
+
785
+ except requests.Timeout:
786
+ return None, "❌ Timeout: Processing took too long."
787
 
788
  except Exception as e:
789
+ import traceback
790
+ error = f"❌ Error: {str(e)}\n\n{traceback.format_exc()}"
791
+ return None, error
792
+
793
+ # ============================================================================
794
+ # HELPER FUNCTIONS - IMPROVED SRT PARSING
795
+ # ============================================================================
796
+
797
+ def parse_srt_string(srt_content):
798
+ """Parse SRT string to subtitle array for Modal API - ROBUST VERSION"""
799
+ subtitles = []
800
+
801
+ # Split by double newline OR by subtitle index pattern
802
+ blocks = re.split(r'\n\s*\n', srt_content.strip())
803
+
804
+ for block in blocks:
805
+ block = block.strip()
806
+ if not block:
807
+ continue
808
+
809
+ lines = block.split('\n')
810
+
811
+ # Need at least 3 lines: index, timestamp, text
812
+ if len(lines) < 3:
813
+ continue
814
+
815
+ try:
816
+ # Line 1: Index (must be a number)
817
+ index_line = lines[0].strip()
818
+
819
+ # Try to extract number from line
820
+ index_match = re.search(r'(\d+)', index_line)
821
+ if not index_match:
822
+ continue
823
+
824
+ index = int(index_match.group(1))
825
+
826
+ # Line 2: Timestamp
827
+ timestamp_line = lines[1].strip()
828
+
829
+ # Parse timestamp: HH:MM:SS,mmm --> HH:MM:SS,mmm
830
+ timestamp_match = re.search(
831
+ r'(\d{2}:\d{2}:\d{2},\d{3})\s*-->\s*(\d{2}:\d{2}:\d{2},\d{3})',
832
+ timestamp_line
833
+ )
834
+
835
+ if not timestamp_match:
836
+ print(f"⚠️ Could not parse timestamp: {timestamp_line}")
837
+ continue
838
+
839
+ start_time = timestamp_match.group(1)
840
+ end_time = timestamp_match.group(2)
841
+
842
+ # Lines 3+: Text (can be multiple lines)
843
+ text = '\n'.join(lines[2:]).strip()
844
+
845
+ if not text:
846
+ print(f"⚠️ Empty text for subtitle {index}")
847
+ continue
848
+
849
+ subtitles.append({
850
+ 'index': index,
851
+ 'start': start_time,
852
+ 'end': end_time,
853
+ 'text': text
854
+ })
855
+
856
+ except Exception as e:
857
+ print(f"⚠️ Error parsing block: {e}")
858
+ print(f"Block content: {block[:100]}")
859
+ continue
860
+
861
+ return subtitles
862
+
863
+ # ============================================================================
864
+ # TEST PARSING FUNCTION
865
+ # ============================================================================
866
+
867
+ def test_srt_parsing(srt_content):
868
+ """Test function to debug SRT parsing"""
869
+ if not srt_content or not srt_content.strip():
870
+ return "❌ No content to parse"
871
+
872
+ subtitles = parse_srt_string(srt_content)
873
+
874
+ result = f"βœ… Parsed {len(subtitles)} subtitles\n\n"
875
+
876
+ if len(subtitles) > 0:
877
+ result += "πŸ“ First 3 subtitles:\n\n"
878
+ for i, sub in enumerate(subtitles[:3]):
879
+ result += f"**Subtitle {sub['index']}:**\n"
880
+ result += f"Time: {sub['start']} --> {sub['end']}\n"
881
+ result += f"Text: {sub['text']}\n\n"
882
+ else:
883
+ result += "❌ No subtitles could be parsed!\n\n"
884
+ result += "**Debug Info:**\n"
885
+ result += f"Content length: {len(srt_content)} chars\n"
886
+ result += f"First 200 chars:\n{srt_content[:200]}\n"
887
+
888
+ return result
889
+
890
+ # ============================================================================
891
+ # GRADIO INTERFACE
892
+ # ============================================================================
893
+
894
+ css = """
895
+ #subtitle_editor {
896
+ font-family: 'Courier New', monospace;
897
+ font-size: 14px;
898
+ }
899
+ .modal-info {
900
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
901
+ color: white;
902
+ padding: 20px;
903
+ border-radius: 10px;
904
+ margin: 15px 0;
905
+ box-shadow: 0 4px 6px rgba(0,0,0,0.1);
906
+ }
907
+ .step-box {
908
+ border: 2px solid #667eea;
909
+ border-radius: 10px;
910
+ padding: 20px;
911
+ margin: 10px 0;
912
+ }
913
+ """
914
+
915
+ with gr.Blocks(title="AI Subtitle Generator & Editor", css=css, theme=gr.themes.Soft()) as demo:
916
+
917
+ # Header
918
+ gr.Markdown(
919
+ """
920
+ # 🎬 AI Subtitle Generator & Editor
921
+
922
+ ### Powered by Whisper Large V3 Turbo on Modal GPU
923
+
924
+ **Two-Step Workflow:**
925
+ 1. πŸ“ **Transcribe** β†’ Upload video, get auto-generated subtitles + preview video
926
+ 2. ✏️ **Edit & Re-burn** (Optional) β†’ Modify subtitles and get updated video
927
+ """
928
+ )
929
+
930
+ # Hidden states
931
+ stored_video = gr.State(None)
932
+ stored_subtitles_json = gr.State("")
933
+
934
+ gr.Markdown("---")
935
+
936
+ # ========================================================================
937
+ # STEP 1: TRANSCRIBE
938
+ # ========================================================================
939
+
940
+ with gr.Group(elem_classes="step-box"):
941
+ gr.Markdown("## πŸ“ Step 1: Upload & Transcribe Video")
942
+ gr.Markdown("*This will automatically generate subtitles AND burn them into the video for preview*")
943
+
944
+ with gr.Row():
945
+ with gr.Column(scale=1):
946
+ video_input = gr.Video(label="Upload Video File")
947
+ language_dropdown = gr.Dropdown(
948
+ choices=["hindi", "english", "auto"],
949
+ value="hindi",
950
+ label="Audio Language",
951
+ info="Select the language spoken in the video"
952
+ )
953
+ transcribe_btn = gr.Button("πŸŽ™οΈ Transcribe & Generate Subtitles", variant="primary", size="lg")
954
+
955
+ with gr.Column(scale=1):
956
+ transcribe_status = gr.Textbox(
957
+ label="Transcription Status",
958
+ lines=15,
959
+ interactive=False
960
+ )
961
+ srt_download = gr.File(
962
+ label="πŸ“₯ Download SRT File",
963
+ interactive=False
964
+ )
965
+
966
+ # Preview video with subtitles
967
+ with gr.Row():
968
+ preview_video = gr.Video(
969
+ label="πŸ“Ή Preview: Video with Generated Subtitles",
970
+ interactive=False
971
+ )
972
+
973
+ gr.Markdown(
974
+ """
975
+ <div class="modal-info">
976
+ ⏱️ <b>Processing Time:</b> 30-60 seconds<br>
977
+ πŸ–₯️ <b>Processing:</b> Modal GPU Server (T4)<br>
978
+ ✨ <b>Output:</b> Video with burned subtitles + Editable SRT file
979
+ </div>
980
+ """
981
+ )
982
+
983
+ gr.Markdown("---")
984
+
985
+ # ========================================================================
986
+ # STEP 2: EDIT & RE-BURN (OPTIONAL)
987
+ # ========================================================================
988
+
989
+ with gr.Group(elem_classes="step-box"):
990
+ gr.Markdown("## ✏️ Step 2: Edit Subtitles & Re-burn (Optional)")
991
+ gr.Markdown("*If you're happy with the preview above, you're done! Otherwise, edit subtitles below and re-burn.*")
992
+
993
+ with gr.Row():
994
+ with gr.Column(scale=1):
995
+ srt_editor = gr.Textbox(
996
+ label="✏️ Editable Subtitles (SRT Format)",
997
+ lines=20,
998
+ placeholder="Subtitles will appear here after transcription...\n\nYou can edit the text, timing, or delete entries.\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...",
999
+ elem_id="subtitle_editor"
1000
+ )
1001
 
1002
+ with gr.Row():
1003
+ test_parse_btn = gr.Button("πŸ§ͺ Test Parsing", size="sm")
1004
+ burn_btn = gr.Button("πŸ”₯ Re-burn with Edited Subtitles", variant="primary", size="lg", scale=2)
1005
+
1006
+ with gr.Column(scale=1):
1007
+ parse_test_output = gr.Textbox(
1008
+ label="πŸ§ͺ Parse Test Results",
1009
+ lines=10,
1010
+ interactive=False,
1011
+ visible=True
1012
+ )
1013
+ burn_status = gr.Textbox(
1014
+ label="Re-burn Status",
1015
+ lines=6,
1016
+ interactive=False
1017
+ )
1018
+ final_video = gr.Video(
1019
+ label="πŸ“Ή Final Video with Edited Subtitles",
1020
+ interactive=False
1021
+ )
1022
+
1023
+ with gr.Accordion("πŸ’‘ Editing Tips", open=False):
1024
+ gr.Markdown(
1025
+ """
1026
+ **How to edit subtitles:**
1027
+ - ✏️ **Change text:** Just modify the text lines
1028
+ - πŸ—‘οΈ **Delete subtitle:** Remove entire block (index + timestamp + text)
1029
+ - ⏰ **Adjust timing:** Edit timestamp line (keep format: `HH:MM:SS,mmm --> HH:MM:SS,mmm`)
1030
+ - βž• **Add subtitle:** Copy format and add new block
1031
+ - πŸ”’ **Re-index:** After deleting, you can renumber indices (1, 2, 3...)
1032
+
1033
+ **Format rules:**
1034
+ ```
1035
+ 1
1036
+ 00:00:00,000 --> 00:00:02,000
1037
+ Your subtitle text here
1038
+
1039
+ 2
1040
+ 00:00:02,000 --> 00:00:04,000
1041
+ Next subtitle text
1042
+ ```
1043
+
1044
+ πŸ’‘ **Use "Test Parsing" button to verify your edits before re-burning!**
1045
+ """
1046
  )
1047
+
1048
+ gr.Markdown(
1049
+ """
1050
+ <div class="modal-info">
1051
+ ⏱️ <b>Processing Time:</b> 5-10 seconds (no re-transcription!)<br>
1052
+ πŸ–₯️ <b>Processing:</b> Modal GPU Server<br>
1053
+ ⚑ <b>Speed:</b> Much faster than re-transcribing
1054
+ </div>
1055
+ """
1056
+ )
1057
+
1058
+ gr.Markdown("---")
1059
+
1060
+ # Footer
1061
+ with gr.Accordion("πŸ“š More Information", open=False):
1062
+ gr.Markdown(
1063
+ f"""
1064
+ ### 🎯 Features:
1065
+
1066
+ - βœ… Automatic subtitle generation using Whisper AI
1067
+ - βœ… Word-level timestamp accuracy
1068
+ - βœ… Instant video preview with subtitles
1069
+ - βœ… Edit subtitles without re-transcription
1070
+ - βœ… Support for Hindi, English, and auto-detection
1071
+ - βœ… Fast GPU processing on Modal
1072
+
1073
+ ### πŸ“Š Technical Details:
1074
+
1075
+ | Component | Details |
1076
+ |-----------|---------|
1077
+ | Backend | Modal Serverless |
1078
+ | GPU | NVIDIA T4 |
1079
+ | Model | Whisper Large V3 Turbo |
1080
+ | Engine | faster-whisper |
1081
+ | Font | Arial, size 20 |
1082
+ | Position | Bottom center |
1083
+ | Max Words | 3 per subtitle |
1084
+
1085
+ ### ⚑ Performance:
1086
+
1087
+ | Operation | Time |
1088
+ |-----------|------|
1089
+ | Transcribe + Burn | 30-60 seconds |
1090
+ | Edit + Re-burn | 5-10 seconds |
1091
+
1092
+ ### πŸ”§ Backend URLs:
1093
+
1094
+ - Transcribe: `{MODAL_TRANSCRIBE_URL}`
1095
+ - Burn: `{MODAL_BURN_URL}`
1096
+ """
1097
+ )
1098
 
1099
+ # ========================================================================
1100
+ # EVENT HANDLERS
1101
+ # ========================================================================
1102
 
1103
+ # Step 1: Transcribe (returns video WITH subtitles)
1104
+ transcribe_btn.click(
1105
+ fn=transcribe_video,
1106
+ inputs=[video_input, language_dropdown],
1107
+ outputs=[stored_video, preview_video, srt_editor, srt_download, transcribe_status, stored_subtitles_json]
1108
+ )
1109
 
1110
+ # Test SRT parsing
1111
+ test_parse_btn.click(
1112
+ fn=test_srt_parsing,
1113
+ inputs=[srt_editor],
1114
+ outputs=[parse_test_output]
1115
+ )
1116
 
1117
+ # Step 2: Re-burn edited subtitles
1118
+ burn_btn.click(
1119
+ fn=burn_edited_subtitles,
1120
+ inputs=[stored_video, srt_editor],
1121
+ outputs=[final_video, burn_status]
1122
+ )
1123
+
1124
+ # ============================================================================
1125
+ # LAUNCH
1126
+ # ============================================================================
1127
 
1128
  if __name__ == "__main__":
1129
+ demo.queue()
1130
+ demo.launch(
 
1131
  share=False,
1132
+ show_error=True,
1133
+ server_name="0.0.0.0",
1134
+ server_port=7860
1135
  )