import gradio as gr import requests import base64 import time import json import re # ============================================================================ # MODAL BACKEND URLS - UPDATED # ============================================================================ MODAL_TRANSCRIBE_URL = "https://vnmalleswar-payyavula--subtitle-generator-test-process-video.modal.run" MODAL_BURN_URL = "https://vnmalleswar-payyavula--subtitle-generator-test-burn-edit-099d8b.modal.run" # ============================================================================ # STEP 1: TRANSCRIBE VIDEO (Calls Modal Backend) # ============================================================================ def transcribe_video(video_file, language="hindi", progress=gr.Progress()): """Call Modal backend to transcribe video - Returns video WITH subtitles burned""" if not video_file: return None, None, None, None, "โ Please upload a video", "" try: progress(0.1, desc="Reading video file...") # Read and encode video with open(video_file, "rb") as f: video_bytes = f.read() video_base64 = base64.b64encode(video_bytes).decode('utf-8') progress(0.2, desc="Uploading to Modal server...") # Prepare request payload = { "video_base64": video_base64, "language": language, "position": "bottom-center" } progress(0.3, desc="Transcribing on GPU server (30-60 seconds)...") # Call Modal transcribe endpoint response = requests.post( MODAL_TRANSCRIBE_URL, json=payload, timeout=600 ) if response.status_code != 200: return None, None, None, None, f"โ Server error: Status {response.status_code}\n{response.text}", "" result = response.json() # Check for errors if "error" in result: error_msg = result["error"] if "traceback" in result: error_msg += f"\n\n{result['traceback']}" return None, None, None, None, f"โ Error: {error_msg}", "" progress(0.9, desc="Processing results...") # Get results srt_content = result.get("srt_content", "") video_base64_output = result.get("video_base64", "") # Video WITH subtitles transcription = result.get("transcription", "") # DEBUG: Print first few lines of SRT print("=" * 60) print("SRT CONTENT RECEIVED:") print(srt_content[:500]) print("=" * 60) # Save SRT file for download timestamp = int(time.time()) srt_path = f"/tmp/subtitles_{timestamp}.srt" with open(srt_path, "w", encoding="utf-8") as f: f.write(srt_content) # Save original video (WITHOUT subtitles) for editing workflow original_video_path = f"/tmp/original_video_{timestamp}.mp4" with open(video_file, "rb") as f: original_bytes = f.read() with open(original_video_path, "wb") as f: f.write(original_bytes) # Save video WITH burned subtitles for preview preview_video_path = f"/tmp/preview_video_{timestamp}.mp4" if video_base64_output: preview_video_bytes = base64.b64decode(video_base64_output) with open(preview_video_path, "wb") as f: f.write(preview_video_bytes) else: preview_video_path = None progress(1.0, desc="Complete!") status = result.get("status", "Success") status += f"\n\n๐ **Statistics:**" status += f"\n- Original words: {result.get('original_word_count', 'N/A')}" status += f"\n- Subtitle words: {result.get('subtitle_word_count', 'N/A')}" status += f"\n- Words lost: {result.get('words_lost', 0)}" status += f"\n- Device: {result.get('device_used', 'N/A')}" status += f"\n- Model: {result.get('model_used', 'N/A')}" status += f"\n\n๐ **Transcription Preview:**\n{transcription[:300]}{'...' if len(transcription) > 300 else ''}" status += f"\n\nโ **Video with subtitles is ready for preview!**" status += f"\n๐ก You can edit subtitles below and re-burn if needed." return original_video_path, preview_video_path, srt_content, srt_path, status, "" except requests.Timeout: return None, None, None, None, "โ Timeout: Processing took too long. Try a shorter video.", "" except Exception as e: import traceback error = f"โ Error: {str(e)}\n\n{traceback.format_exc()}" return None, None, None, None, error, "" # ============================================================================ # STEP 2: BURN EDITED SUBTITLES (Calls Modal Backend) # ============================================================================ def burn_edited_subtitles(original_video_path, edited_srt_content, progress=gr.Progress()): """Call Modal backend to burn edited subtitles""" if not original_video_path: return None, "โ No video found. Please transcribe a video first in Step 1." if not edited_srt_content or not edited_srt_content.strip(): return None, "โ No subtitles provided. Please edit subtitles first." try: progress(0.1, desc="Reading video file...") # Read original video with open(original_video_path, "rb") as f: video_bytes = f.read() video_base64 = base64.b64encode(video_bytes).decode('utf-8') progress(0.2, desc="Parsing edited subtitles...") # Parse edited SRT to subtitles array subtitles = parse_srt_string(edited_srt_content) print("=" * 60) print(f"PARSED {len(subtitles)} SUBTITLES:") for i, sub in enumerate(subtitles[:3]): print(f"Subtitle {i+1}: {sub}") print("=" * 60) if not subtitles: 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" progress(0.3, desc="Uploading to Modal server...") # Prepare request payload = { "video_base64": video_base64, "subtitles": subtitles, "position": "bottom-center" } progress(0.4, desc="Burning subtitles on server (5-10 seconds)...") print(f"Sending {len(subtitles)} subtitles to Modal...") # Call Modal burn endpoint response = requests.post( MODAL_BURN_URL, json=payload, timeout=300 ) print(f"Response status: {response.status_code}") if response.status_code != 200: return None, f"โ Server error: Status {response.status_code}\n{response.text}" result = response.json() # Check for errors if "error" in result: error_msg = result["error"] if "traceback" in result: error_msg += f"\n\n{result['traceback']}" return None, f"โ Error: {error_msg}" progress(0.8, desc="Downloading processed video...") # Get output video video_base64_output = result.get("video_base64", "") if not video_base64_output: return None, "โ No video returned from server" # Save final video timestamp = int(time.time()) final_video_path = f"/tmp/final_video_{timestamp}.mp4" video_bytes = base64.b64decode(video_base64_output) with open(final_video_path, "wb") as f: f.write(video_bytes) print(f"Saved final video to {final_video_path}") progress(1.0, desc="Complete!") status = result.get("status", "Success") status += f"\n\nโ Video ready! Burned {len(subtitles)} edited subtitles." status += f"\n\n๐ **Debug Info:**" status += f"\n- Subtitles sent: {len(subtitles)}" status += f"\n- Server response: {result.get('subtitle_count', 'N/A')}" return final_video_path, status except requests.Timeout: return None, "โ Timeout: Processing took too long." except Exception as e: import traceback error = f"โ Error: {str(e)}\n\n{traceback.format_exc()}" return None, error # ============================================================================ # HELPER FUNCTIONS - IMPROVED SRT PARSING # ============================================================================ def parse_srt_string(srt_content): """Parse SRT string to subtitle array for Modal API - ROBUST VERSION""" subtitles = [] # Split by double newline OR by subtitle index pattern blocks = re.split(r'\n\s*\n', srt_content.strip()) for block in blocks: block = block.strip() if not block: continue lines = block.split('\n') # Need at least 3 lines: index, timestamp, text if len(lines) < 3: continue try: # Line 1: Index (must be a number) index_line = lines[0].strip() # Try to extract number from line index_match = re.search(r'(\d+)', index_line) if not index_match: continue index = int(index_match.group(1)) # Line 2: Timestamp timestamp_line = lines[1].strip() # Parse timestamp: HH:MM:SS,mmm --> HH:MM:SS,mmm timestamp_match = re.search( r'(\d{2}:\d{2}:\d{2},\d{3})\s*-->\s*(\d{2}:\d{2}:\d{2},\d{3})', timestamp_line ) if not timestamp_match: print(f"โ ๏ธ Could not parse timestamp: {timestamp_line}") continue start_time = timestamp_match.group(1) end_time = timestamp_match.group(2) # Lines 3+: Text (can be multiple lines) text = '\n'.join(lines[2:]).strip() if not text: print(f"โ ๏ธ Empty text for subtitle {index}") continue subtitles.append({ 'index': index, 'start': start_time, 'end': end_time, 'text': text }) except Exception as e: print(f"โ ๏ธ Error parsing block: {e}") print(f"Block content: {block[:100]}") continue return subtitles # ============================================================================ # TEST PARSING FUNCTION # ============================================================================ def test_srt_parsing(srt_content): """Test function to debug SRT parsing""" if not srt_content or not srt_content.strip(): return "โ No content to parse" subtitles = parse_srt_string(srt_content) result = f"โ Parsed {len(subtitles)} subtitles\n\n" if len(subtitles) > 0: result += "๐ First 3 subtitles:\n\n" for i, sub in enumerate(subtitles[:3]): result += f"**Subtitle {sub['index']}:**\n" result += f"Time: {sub['start']} --> {sub['end']}\n" result += f"Text: {sub['text']}\n\n" else: result += "โ No subtitles could be parsed!\n\n" result += "**Debug Info:**\n" result += f"Content length: {len(srt_content)} chars\n" result += f"First 200 chars:\n{srt_content[:200]}\n" return result # ============================================================================ # GRADIO INTERFACE # ============================================================================ css = """ #subtitle_editor { font-family: 'Courier New', monospace; font-size: 14px; } .modal-info { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; padding: 20px; border-radius: 10px; margin: 15px 0; box-shadow: 0 4px 6px rgba(0,0,0,0.1); } .step-box { border: 2px solid #667eea; border-radius: 10px; padding: 20px; margin: 10px 0; } """ with gr.Blocks(title="AI Subtitle Generator & Editor", css=css, theme=gr.themes.Soft()) as demo: # Header gr.Markdown( """ # ๐ฌ AI Subtitle Generator & Editor ### Powered by Whisper Large V3 Turbo on Modal GPU **Two-Step Workflow:** 1. ๐ **Transcribe** โ Upload video, get auto-generated subtitles + preview video 2. โ๏ธ **Edit & Re-burn** (Optional) โ Modify subtitles and get updated video """ ) # Hidden states stored_video = gr.State(None) stored_subtitles_json = gr.State("") gr.Markdown("---") # ======================================================================== # STEP 1: TRANSCRIBE # ======================================================================== with gr.Group(elem_classes="step-box"): gr.Markdown("## ๐ Step 1: Upload & Transcribe Video") gr.Markdown("*This will automatically generate subtitles AND burn them into the video for preview*") with gr.Row(): with gr.Column(scale=1): video_input = gr.Video(label="Upload Video File") language_dropdown = gr.Dropdown( choices=["hindi", "english", "auto"], value="hindi", label="Audio Language", info="Select the language spoken in the video" ) transcribe_btn = gr.Button("๐๏ธ Transcribe & Generate Subtitles", variant="primary", size="lg") with gr.Column(scale=1): transcribe_status = gr.Textbox( label="Transcription Status", lines=15, interactive=False ) srt_download = gr.File( label="๐ฅ Download SRT File", interactive=False ) # Preview video with subtitles with gr.Row(): preview_video = gr.Video( label="๐น Preview: Video with Generated Subtitles", interactive=False ) gr.Markdown( """