Spaces:
Running
Running
| import gradio as gr | |
| import base64 | |
| import json | |
| import re | |
| from datetime import datetime | |
| from io import BytesIO | |
| # JavaScript code for video processing and OCR | |
| VIDEO_PROCESSING_JS = """ | |
| function() { | |
| const state = { | |
| videoLoaded: false, | |
| videoFile: null, | |
| subtitles: [], | |
| currentTime: 0, | |
| duration: 0, | |
| isPlaying: false, | |
| isMuted: false, | |
| subtitleBox: { x: 10, y: 70, width: 80, height: 20 }, | |
| processing: false, | |
| ocrWorker: null, | |
| currentSource: 'file', | |
| cameraActive: false, | |
| liveProcessing: false, | |
| liveStartTime: null, | |
| liveProcessingInterval: null | |
| }; | |
| // Language codes for Tesseract | |
| const LANGUAGES = { | |
| 'English': 'eng', | |
| 'Japanese': 'jpn', | |
| 'Korean': 'kor', | |
| 'Chinese (Simplified)': 'chi_sim', | |
| 'Chinese (Traditional)': 'chi_tra', | |
| 'Thai': 'tha', | |
| 'Vietnamese': 'vie', | |
| 'Hindi': 'hin', | |
| 'Arabic': 'ara', | |
| 'Russian': 'rus', | |
| 'Spanish': 'spa', | |
| 'French': 'fra', | |
| 'German': 'deu', | |
| 'Portuguese': 'por', | |
| 'Italian': 'ita', | |
| 'Dutch': 'dut', | |
| 'Polish': 'pol', | |
| 'Turkish': 'tur' | |
| }; | |
| return { state: state, languages: LANGUAGES }; | |
| } | |
| """ | |
| def create_video_ocr_app(): | |
| """Create a comprehensive Video Subtitle OCR Extractor application""" | |
| with gr.Blocks() as demo: | |
| # Header with branding | |
| gr.Markdown(""" | |
| <div style="text-align: center; padding: 20px 0; border-bottom: 1px solid #2d2d3a; margin-bottom: 30px;"> | |
| <h1 style="font-size: 2.5rem; margin-bottom: 10px; background: linear-gradient(135deg, #6366f1, #ec4899); -webkit-background-clip: text; -webkit-text-fill-color: transparent;"> | |
| 🎬 Video Subtitle OCR Extractor | |
| </h1> | |
| <p style="color: #94a3b8; font-size: 1.1rem;"> | |
| Extract hardcoded subtitles from any video using OCR technology | |
| </p> | |
| <a href="https://huggingface.co/spaces/akhaliq/anycoder" target="_blank" style="color: #6366f1; text-decoration: none; font-size: 0.9rem;"> | |
| Built with anycoder | |
| </a> | |
| </div> | |
| """) | |
| with gr.Row(): | |
| # Left Column - Video Upload and Preview | |
| with gr.Column(scale=2): | |
| # Video Upload Section | |
| gr.Markdown("### 📁 Video Source") | |
| with gr.Tabs(): | |
| with gr.Tab("📂 Upload File"): | |
| video_input = gr.Video( | |
| label="Upload Video File", | |
| sources=["upload"], | |
| height=400, | |
| elem_id="main-video" | |
| ) | |
| gr.Markdown("*Supports MP4, WebM, MOV, AVI formats*") | |
| with gr.Tab("📷 Live Camera"): | |
| gr.Markdown(""" | |
| <div style="background: #1a1a25; padding: 20px; border-radius: 12px; text-align: center;"> | |
| <p style="color: #94a3b8; margin-bottom: 10px;">Use live camera for real-time subtitle extraction</p> | |
| <p style="color: #64748b; font-size: 0.85rem;">📷 Click "Start Camera" to begin recording</p> | |
| </div> | |
| """) | |
| live_camera_btn = gr.Button("📷 Start Live Camera", variant="primary") | |
| live_camera_status = gr.Textbox(label="Camera Status", value="Not active", interactive=False) | |
| # Video Preview | |
| gr.Markdown("### ▶️ Video Preview") | |
| with gr.Row(): | |
| with gr.Column(scale=3): | |
| video_preview = gr.Video( | |
| label="Video Preview", | |
| height=350, | |
| interactive=False, | |
| elem_id="video-preview" | |
| ) | |
| with gr.Column(scale=1): | |
| gr.Markdown("**Playback Controls**") | |
| play_btn = gr.Button("▶ Play", variant="primary") | |
| pause_btn = gr.Button("⏸ Pause") | |
| restart_btn = gr.Button("⏮ Restart") | |
| current_time_display = gr.Textbox( | |
| label="Current Time", | |
| value="00:00:00", | |
| interactive=False | |
| ) | |
| # Subtitle Region Selection | |
| gr.Markdown("### 📐 Subtitle Region Selection") | |
| with gr.Row(): | |
| with gr.Column(scale=1): | |
| x_pos = gr.Slider( | |
| minimum=0, maximum=90, value=10, step=1, | |
| label="Horizontal Position (X)", | |
| info="X-axis position of subtitle box" | |
| ) | |
| with gr.Column(scale=1): | |
| y_pos = gr.Slider( | |
| minimum=0, maximum=90, value=70, step=1, | |
| label="Vertical Position (Y)", | |
| info="Y-axis position of subtitle box" | |
| ) | |
| with gr.Row(): | |
| with gr.Column(scale=1): | |
| box_width = gr.Slider( | |
| minimum=10, maximum=100, value=80, step=1, | |
| label="Width (%)", | |
| info="Width of subtitle detection area" | |
| ) | |
| with gr.Column(scale=1): | |
| box_height = gr.Slider( | |
| minimum=5, maximum=40, value=20, step=1, | |
| label="Height (%)", | |
| info="Height of subtitle detection area" | |
| ) | |
| with gr.Row(): | |
| reset_region_btn = gr.Button("↺ Reset Region", variant="secondary") | |
| lock_aspect_btn = gr.Button("🔒 Lock Aspect Ratio", variant="secondary") | |
| # Time Range Selection | |
| gr.Markdown("### ⏱️ Processing Time Range") | |
| with gr.Row(): | |
| with gr.Column(scale=1): | |
| start_time_input = gr.Textbox( | |
| label="Start Time", | |
| placeholder="Leave empty for start (e.g., 00:01:30)", | |
| value="" | |
| ) | |
| set_start_btn = gr.Button("Set to Current Time") | |
| with gr.Column(scale=1): | |
| end_time_input = gr.Textbox( | |
| label="End Time", | |
| placeholder="Leave empty for end (e.g., 00:05:00)", | |
| value="" | |
| ) | |
| set_end_btn = gr.Button("Set to Current Time") | |
| # Language Selection | |
| gr.Markdown("### 🌐 OCR Language") | |
| with gr.Row(): | |
| language_select = gr.Dropdown( | |
| choices=[ | |
| "English", "Japanese", "Korean", "Chinese (Simplified)", | |
| "Chinese (Traditional)", "Thai", "Vietnamese", "Hindi", | |
| "Arabic", "Russian", "Spanish", "French", "German", | |
| "Portuguese", "Italian", "Dutch", "Polish", "Turkish" | |
| ], | |
| value="English", | |
| label="Select OCR Language", | |
| info="Prioritizes selected language for better accuracy" | |
| ) | |
| language_priority = gr.Checkbox( | |
| label="Enable Language Priority", | |
| value=True, | |
| info="Use language-specific OCR optimizations" | |
| ) | |
| # Process Button | |
| with gr.Row(): | |
| process_btn = gr.Button( | |
| "⚡ Start OCR Processing", | |
| variant="primary", | |
| size="lg", | |
| scale=2 | |
| ) | |
| stop_process_btn = gr.Button("⏹ Stop", variant="stop", size="lg") | |
| progress_bar = gr.Slider( | |
| minimum=0, maximum=100, value=0, | |
| label="Processing Progress", | |
| interactive=False, | |
| visible=False | |
| ) | |
| status_text = gr.Textbox( | |
| label="Status", | |
| value="Ready", | |
| interactive=False | |
| ) | |
| # Right Column - Results | |
| with gr.Column(scale=1): | |
| # Options | |
| gr.Markdown("### ⚙️ Options") | |
| with gr.Row(): | |
| show_overlay = gr.Checkbox( | |
| label="Show Selection Box", | |
| value=True | |
| ) | |
| auto_play = gr.Checkbox( | |
| label="Auto Play Preview", | |
| value=True | |
| ) | |
| with gr.Row(): | |
| highlight_active = gr.Checkbox( | |
| label="Highlight Active Subtitles", | |
| value=True | |
| ) | |
| preserve_timing = gr.Checkbox( | |
| label="Frame-accurate Timing", | |
| value=True | |
| ) | |
| # Results | |
| gr.Markdown("### 📝 Extracted Subtitles") | |
| subtitle_count = gr.Textbox( | |
| value="0 lines extracted", | |
| label="Results", | |
| interactive=False | |
| ) | |
| subtitles_output = gr.Dataframe( | |
| headers=["#", "Start Time", "End Time", "Subtitle Text"], | |
| datatype=["number", "str", "str", "str"], | |
| label="Subtitles", | |
| interactive=False, | |
| height=400 | |
| ) | |
| # Active subtitle display | |
| active_subtitle = gr.Textbox( | |
| label="Currently Active Subtitle", | |
| value="", | |
| interactive=False, | |
| lines=3 | |
| ) | |
| # Export Buttons | |
| gr.Markdown("### 💾 Export") | |
| with gr.Row(): | |
| export_srt = gr.Button("📄 Export SRT", variant="primary") | |
| export_txt = gr.Button("📝 Export TXT") | |
| export_json = gr.Button("📋 Export JSON") | |
| clear_btn = gr.Button("🗑️ Clear All", variant="stop") | |
| # Statistics | |
| gr.Markdown("### 📊 Statistics") | |
| with gr.Row(): | |
| total_lines = gr.Number(value=0, label="Total Lines", interactive=False) | |
| processing_time = gr.Textbox(label="Processing Time", value="-", interactive=False) | |
| detected_language = gr.Textbox(label="Detected Language", value="-", interactive=False) | |
| # Event Handlers | |
| def update_preview(video_path): | |
| """Update video preview when file is uploaded""" | |
| if video_path: | |
| return video_path | |
| return None | |
| def set_start_time(current_time): | |
| """Set start time from current playback position""" | |
| return format_time(current_time) | |
| def set_end_time(current_time): | |
| """Set end time from current playback position""" | |
| return format_time(current_time) | |
| def reset_region(): | |
| """Reset subtitle region to defaults""" | |
| return 10, 70, 80, 20 | |
| def format_time(seconds): | |
| """Format seconds to HH:MM:SS""" | |
| hours = int(seconds // 3600) | |
| minutes = int((seconds % 3600) // 60) | |
| secs = int(seconds % 60) | |
| return f"{hours:02d}:{minutes:02d}:{secs:02d}" | |
| def parse_time(time_str): | |
| """Parse time string to seconds""" | |
| if not time_str: | |
| return None | |
| try: | |
| parts = time_str.split(':') | |
| if len(parts) == 3: | |
| return int(parts[0]) * 3600 + int(parts[1]) * 60 + int(parts[2]) | |
| elif len(parts) == 2: | |
| return int(parts[0]) * 60 + int(parts[1]) | |
| return float(time_str) | |
| except: | |
| return None | |
| def generate_srt(subtitles): | |
| """Generate SRT format subtitles""" | |
| if not subtitles: | |
| return "" | |
| srt_content = "" | |
| for i, sub in enumerate(subtitles, 1): | |
| srt_content += f"{i}\\n" | |
| srt_content += f"{sub['start']} --> {sub['end']}\\n" | |
| srt_content += f"{sub['text']}\\n\\n" | |
| return srt_content | |
| def generate_txt(subtitles): | |
| """Generate plain text subtitles""" | |
| if not subtitles: | |
| return "" | |
| return "\\n".join([sub['text'] for sub in subtitles]) | |
| def generate_json(subtitles): | |
| """Generate JSON format subtitles""" | |
| return json.dumps(subtitles, indent=2, ensure_ascii=False) | |
| # Connect video upload to preview | |
| video_input.change( | |
| fn=update_preview, | |
| inputs=[video_input], | |
| outputs=[video_preview] | |
| ) | |
| # Set time buttons | |
| set_start_btn.click( | |
| fn=set_start_time, | |
| inputs=[current_time_display], | |
| outputs=[start_time_input] | |
| ) | |
| set_end_btn.click( | |
| fn=set_end_time, | |
| inputs=[current_time_display], | |
| outputs=[end_time_input] | |
| ) | |
| # Reset region | |
| reset_region_btn.click( | |
| fn=reset_region, | |
| inputs=[], | |
| outputs=[x_pos, y_pos, box_width, box_height] | |
| ) | |
| # Process button - Main OCR processing | |
| def process_video( | |
| video_path, | |
| x, y, width, height, | |
| start_time_str, end_time_str, | |
| language, use_priority, | |
| highlight, preserve_time | |
| ): | |
| """Process video and extract subtitles using OCR""" | |
| if not video_path: | |
| return ( | |
| "No video loaded. Please upload a video first.", | |
| [], | |
| "0 lines extracted", | |
| "", 0, "-", "-" | |
| ) | |
| start_time = parse_time(start_time_str) if start_time_str else 0 | |
| end_time = parse_time(end_time_str) | |
| # Return processing info - actual OCR happens client-side | |
| yield ( | |
| "Video loaded. Starting OCR extraction...", | |
| [], | |
| "0 lines extracted", | |
| "", 0, "-", language | |
| ) | |
| # Note: Actual OCR processing requires client-side JavaScript | |
| # This is a placeholder that returns processing parameters | |
| processing_params = { | |
| 'video_path': video_path, | |
| 'region': {'x': x, 'y': y, 'width': width, 'height': height}, | |
| 'start_time': start_time, | |
| 'end_time': end_time, | |
| 'language': language, | |
| 'use_priority': use_priority, | |
| 'highlight': highlight, | |
| 'preserve_time': preserve_time | |
| } | |
| return ( | |
| f"Processing with params: {json.dumps(processing_params)}", | |
| [], | |
| "Processing...", | |
| "", 0, "-", language | |
| ) | |
| # Export handlers | |
| def export_subtitles_srt(subtitles_json): | |
| """Export subtitles as SRT""" | |
| if isinstance(subtitles_json, str): | |
| subtitles = json.loads(subtitles_json) if subtitles_json else [] | |
| else: | |
| subtitles = subtitles_json | |
| return generate_srt(subtitles) | |
| def export_subtitles_txt(subtitles_json): | |
| """Export subtitles as TXT""" | |
| if isinstance(subtitles_json, str): | |
| subtitles = json.loads(subtitles_json) if subtitles_json else [] | |
| else: | |
| subtitles = subtitles_json | |
| return generate_txt(subtitles) | |
| def export_subtitles_json(subtitles_json): | |
| """Export subtitles as JSON""" | |
| if isinstance(subtitles_json, str): | |
| subtitles = json.loads(subtitles_json) if subtitles_json else [] | |
| else: | |
| subtitles = subtitles_json | |
| return generate_json(subtitles) | |
| # Connect export buttons | |
| export_srt.click( | |
| fn=export_subtitles_srt, | |
| inputs=[subtitles_output], | |
| outputs=[subtitles_output] | |
| ) | |
| # Playback controls | |
| play_btn.click( | |
| fn=lambda: "Playing...", | |
| inputs=[], | |
| outputs=[status_text] | |
| ) | |
| pause_btn.click( | |
| fn=lambda: "Paused", | |
| inputs=[], | |
| outputs=[status_text] | |
| ) | |
| # Clear all | |
| def clear_all(): | |
| return None, "", "", [], "0 lines extracted", "", 0, "-", "-", "-" | |
| clear_btn.click( | |
| fn=clear_all, | |
| inputs=[], | |
| outputs=[ | |
| video_input, start_time_input, end_time_input, | |
| subtitles_output, subtitle_count, active_subtitle, | |
| total_lines, processing_time, detected_language, video_preview | |
| ] | |
| ) | |
| return demo | |
| def format_time(seconds): | |
| """Format seconds to HH:MM:SS,mmm""" | |
| if seconds is None: | |
| return "00:00:00,000" | |
| hours = int(seconds // 3600) | |
| minutes = int((seconds % 3600) // 60) | |
| secs = int(seconds % 60) | |
| millis = int((seconds % 1) * 1000) | |
| return f"{hours:02d}:{minutes:02d}:{secs:02d},{millis:03d}" | |
| def format_time_simple(seconds): | |
| """Format seconds to HH:MM:SS""" | |
| if seconds is None: | |
| return "00:00:00" | |
| hours = int(seconds // 3600) | |
| minutes = int((seconds % 3600) // 60) | |
| secs = int(seconds % 60) | |
| return f"{hours:02d}:{minutes:02d}:{secs:02d}" | |
| def parse_time(time_str): | |
| """Parse time string (HH:MM:SS or HH:MM:SS,mmm) to seconds""" | |
| if not time_str: | |
| return None | |
| try: | |
| # Handle both comma and dot milliseconds | |
| time_str = time_str.replace(',', '.') | |
| parts = time_str.split(':') | |
| if len(parts) == 3: | |
| hours = int(parts[0]) | |
| minutes = int(parts[1]) | |
| seconds = float(parts[2]) | |
| return hours * 3600 + minutes * 60 + seconds | |
| elif len(parts) == 2: | |
| minutes = int(parts[0]) | |
| seconds = float(parts[1]) | |
| return minutes * 60 + seconds | |
| return float(time_str) | |
| except (ValueError, AttributeError): | |
| return None | |
| # Create and launch the demo | |
| if __name__ == "__main__": | |
| demo = create_video_ocr_app() | |
| demo.launch( | |
| title="Video Subtitle OCR Extractor", | |
| theme=gr.themes.Soft( | |
| primary_hue="indigo", | |
| secondary_hue="pink", | |
| neutral_hue="slate", | |
| font=gr.themes.GoogleFont("Inter"), | |
| text_size="lg", | |
| spacing_size="lg", | |
| radius_size="md" | |
| ), | |
| css=""" | |
| .gradio-container {max-width: 1600px !important;} | |
| #main-video video {max-height: 400px;} | |
| #video-preview video {max-height: 350px;} | |
| """, | |
| footer_links=[ | |
| {"label": "Video Subtitle OCR Extractor", "url": "#"}, | |
| {"label": "Built with Gradio", "url": "https://gradio.app"}, | |
| {"label": "Powered by Tesseract.js", "url": "https://tesseract.projectnaptha.com/"} | |
| ] | |
| ) |