Wahso's picture
Upload folder using huggingface_hub
dd0514f verified
Raw
History Blame Contribute Delete
21.6 kB
"""
Video Hard-Subtitle OCR Application
Extract text from video hard subtitles with customizable region selection
"""
import gradio as gr
import cv2
import numpy as np
import json
import os
import tempfile
from datetime import datetime
from pathlib import Path
# Try to import optional dependencies
try:
import moviepy.editor as mp
MOVIEPY_AVAILABLE = True
except ImportError:
MOVIEPY_AVAILABLE = False
try:
import easyocr
EASYOCR_AVAILABLE = True
except ImportError:
EASYOCR_AVAILABLE = False
# Language to OCR language code mapping
LANGUAGE_CODES = {
"English": "en",
"Japanese": "ja",
"Korean": "ko",
"Chinese (Simplified)": "ch_sim",
"Chinese (Traditional)": "ch_tra",
"Thai": "th"
}
# OCR Reader cache
_reader_cache = {}
def get_ocr_reader(language: str):
"""Get or create OCR reader for specified language"""
lang_code = LANGUAGE_CODES.get(language, "en")
if lang_code in _reader_cache:
return _reader_cache[lang_code]
if EASYOCR_AVAILABLE:
try:
reader = easyocr.Reader([lang_code], gpu=False, verbose=False)
_reader_cache[lang_code] = reader
return reader
except Exception as e:
return None
return None
def extract_frames_from_video(video_path: str, start_time: float, end_time: float):
"""Extract frames from video in the specified time range"""
frames = []
timestamps = []
cap = cv2.VideoCapture(video_path)
if not cap.isOpened():
return frames, timestamps
fps = cap.get(cv2.CAP_PROP_FPS)
total_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
duration = total_frames / fps if fps > 0 else 0
start_frame = int(start_time * fps)
end_frame = int(end_time * fps)
cap.set(cv2.CAP_PROP_POS_FRAMES, start_frame)
current_frame = start_frame
while current_frame <= end_frame:
ret, frame = cap.read()
if not ret:
break
timestamp = current_frame / fps
frames.append(frame)
timestamps.append(timestamp)
current_frame += 1
cap.release()
return frames, timestamps
def extract_subtitle_from_frame(frame, x: int, y: int, width: int, height: int, reader):
"""Extract text from a specific region of a frame"""
h, w = frame.shape[:2]
# Ensure coordinates are within bounds
x = max(0, min(x, w - 1))
y = max(0, min(y, h - 1))
width = min(width, w - x)
height = min(height, h - y)
if width <= 0 or height <= 0:
return ""
# Crop the subtitle region
roi = frame[y:y+height, x:x+width]
# Convert to RGB for OCR
roi_rgb = cv2.cvtColor(roi, cv2.COLOR_BGR2RGB)
try:
results = reader.readtext(roi_rgb)
if results:
# Get the text with highest confidence
text = " ".join([result[1] for result in results])
return text.strip()
except Exception:
pass
return ""
def merge_subtitles(subtitles: list, time_threshold: float = 1.0, iou_threshold: float = 0.5):
"""Merge consecutive subtitles that are similar or close in time"""
if not subtitles:
return []
merged = []
current = subtitles[0].copy()
for i in range(1, len(subtitles)):
next_sub = subtitles[i]
# Check if they overlap in time
time_gap = next_sub["start"] - current["end"]
# Check if text is similar (simple check)
text_similar = (
next_sub["text"].strip().lower() == current["text"].strip().lower()
or next_sub["text"].strip() in current["text"].strip()
)
if time_gap <= time_threshold and text_similar:
# Extend current subtitle
current["end"] = next_sub["end"]
else:
merged.append(current)
current = next_sub.copy()
merged.append(current)
return merged
def process_video_ocr(
video_path: str,
x: int, y: int, width: int, height: int,
language: str,
start_time: float, end_time: float,
progress=gr.Progress()
):
"""Main OCR processing function"""
if not video_path:
return None, "Please upload a video first.", ""
if not EASYOCR_AVAILABLE:
return None, "EasyOCR is not installed. Please install it with: pip install easyocr", ""
reader = get_ocr_reader(language)
if reader is None:
return None, f"Could not initialize OCR for {language}", ""
# Extract frames
progress(0.1, "Extracting frames from video...")
frames, timestamps = extract_frames_from_video(video_path, start_time, end_time)
if not frames:
return None, "Could not extract frames from video", ""
# Process frames
subtitles = []
frame_interval = max(1, len(frames) // 100) # Sample ~100 points
for i, (frame, timestamp) in enumerate(frames):
if i % frame_interval == 0:
progress(0.1 + 0.7 * (i / len(frames)), f"Processing frame {i}/{len(frames)}...")
text = extract_subtitle_from_frame(frame, x, y, width, height, reader)
if text and text.strip():
# Check if this is a new subtitle or continuation
if subtitles and (timestamp - subtitles[-1]["end"]) < 0.5:
# Might be continuation, check text similarity
if text.strip() != subtitles[-1]["text"].strip():
subtitles.append({
"start": timestamp,
"end": timestamp + 0.1,
"text": text.strip()
})
else:
subtitles.append({
"start": timestamp,
"end": timestamp + 0.1,
"text": text.strip()
})
# Merge similar subtitles
progress(0.85, "Merging subtitle entries...")
subtitles = merge_subtitles(subtitles)
# Assign sequential IDs
for i, sub in enumerate(subtitles):
sub["id"] = i + 1
# Generate SRT
progress(0.95, "Generating output files...")
srt_output = generate_srt(subtitles)
txt_output = generate_txt(subtitles)
json_output = generate_json(subtitles, language, start_time, end_time)
progress(1.0, "Complete!")
return subtitles, srt_output, json_output
def generate_srt(subtitles: list) -> str:
"""Generate SRT format output"""
srt_lines = []
for sub in subtitles:
sub_id = sub.get("id", 1)
start_time = format_srt_time(sub["start"])
end_time = format_srt_time(sub["end"])
text = sub["text"]
srt_lines.append(f"{sub_id}")
srt_lines.append(f"{start_time} --> {end_time}")
srt_lines.append(text)
srt_lines.append("")
return "\n".join(srt_lines)
def format_srt_time(seconds: float) -> str:
"""Format time for SRT (HH:MM:SS,mmm)"""
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 generate_txt(subtitles: list) -> str:
"""Generate plain text output"""
return "\n".join([sub["text"] for sub in subtitles])
def generate_json(subtitles: list, language: str, start: float, end: float) -> str:
"""Generate JSON output"""
output = {
"metadata": {
"language": language,
"start_time": start,
"end_time": end,
"total_subtitles": len(subtitles),
"generated_at": datetime.now().isoformat()
},
"subtitles": subtitles
}
return json.dumps(output, indent=2, ensure_ascii=False)
def get_video_info(video_path: str):
"""Get video information"""
if not video_path or not os.path.exists(video_path):
return 0, 0, 0, 0
cap = cv2.VideoCapture(video_path)
if not cap.isOpened():
return 0, 0, 0, 0
width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))
height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
fps = cap.get(cv2.CAP_PROP_FPS)
frame_count = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
duration = frame_count / fps if fps > 0 else 0
cap.release()
return width, height, fps, duration
def create_demo():
"""Create the Gradio demo"""
with gr.Blocks(title="Video Subtitle OCR") as demo:
# Header with anycoder link
gr.HTML("""
<div style="text-align: center; padding: 10px; background: linear-gradient(90deg, #667eea 0%, #764ba2 100%); border-radius: 10px; margin-bottom: 20px;">
<h1 style="color: white; margin: 0;">🎬 Video Subtitle OCR</h1>
<p style="color: #e0e0e0; margin: 5px 0;">Extract hard-coded subtitles from any video</p>
<a href="https://huggingface.co/spaces/akhaliq/anycoder" target="_blank" style="color: #ffd700; font-weight: bold;">Built with anycoder</a>
</div>
""")
with gr.Row():
with gr.Column(scale=2):
# Video upload and preview
video_input = gr.Video(
label="Upload Video",
sources=["upload"],
height=400
)
# Video info display
video_info = gr.JSON(label="Video Information", visible=False)
with gr.Column(scale=1):
# Settings panel
gr.Markdown("### ⚙️ Settings")
# Language selection
language = gr.Dropdown(
choices=list(LANGUAGE_CODES.keys()),
value="English",
label="OCR Language",
info="Select the primary language for OCR accuracy"
)
# Time range
with gr.Row():
start_time = gr.Number(
label="Start Time (sec)",
value=0,
minimum=0,
step=0.1
)
end_time = gr.Number(
label="End Time (sec)",
value=10,
minimum=0,
step=0.1
)
# Subtitle region adjustment
gr.Markdown("### 📐 Adjust Subtitle Area")
with gr.Row():
with gr.Column():
# Position sliders
x_pos = gr.Slider(
minimum=0, maximum=100, value=10,
label="Horizontal Position (X %)",
info="X-axis position of subtitle box"
)
y_pos = gr.Slider(
minimum=0, maximum=100, value=80,
label="Vertical Position (Y %)",
info="Y-axis position of subtitle box"
)
with gr.Column():
# Size sliders
box_width = gr.Slider(
minimum=10, maximum=100, value=80,
label="Width (%)",
info="Width of subtitle box"
)
box_height = gr.Slider(
minimum=5, maximum=50, value=15,
label="Height (%)",
info="Height of subtitle box"
)
# Options
with gr.Row():
aspect_lock = gr.Checkbox(
value=True,
label="Lock Aspect Ratio",
info="Maintain 16:9 aspect ratio for subtitle box"
)
auto_process = gr.Checkbox(
value=False,
label="Auto-process on change",
info="Automatically process when parameters change"
)
# Reset button
with gr.Row():
reset_btn = gr.Button("↺ Reset to Defaults", variant="secondary")
# Process button
process_btn = gr.Button("🔍 Process Video", variant="primary", size="lg")
# Progress indicator
progress_bar = gr.Progress()
# Results section
gr.Markdown("### 📄 Results")
with gr.Row():
with gr.Column():
# Subtitle list with status
subtitle_list = gr.Dataframe(
headers=["ID", "Start", "End", "Status", "Text"],
datatype=["number", "str", "str", "str", "str"],
label="Extracted Subtitles",
wrap=True,
max_height=400
)
with gr.Column():
# Export options
with gr.Accordion("📥 Export Options", open=True):
srt_output = gr.Textbox(
label="SRT Format",
lines=10,
max_lines=20
)
with gr.Row():
download_srt = gr.DownloadButton(
label="Download SRT",
variant="secondary"
)
download_txt = gr.DownloadButton(
label="Download TXT",
variant="secondary"
)
download_json = gr.DownloadButton(
label="Download JSON",
variant="secondary"
)
# Preview section
gr.Markdown("### 👁️ Active Subtitle Preview")
subtitle_preview = gr.JSON(
label="Active Subtitles Status",
info="Shows subtitle status during video playback"
)
# Hidden state for processed data
processed_data = gr.State()
# Event handlers
def update_video_info(video_path):
"""Update video information when video is uploaded"""
if video_path:
width, height, fps, duration = get_video_info(video_path)
info = {
"Resolution": f"{width} x {height}",
"FPS": round(fps, 2) if fps else 0,
"Duration": f"{duration:.2f}s" if duration else "0s"
}
return info, {"visible": True}, duration
return {}, {"visible": False}, 0
def update_end_time_on_load(info, duration):
"""Update end time when video is loaded"""
if duration > 0:
return duration
return 10
def on_aspect_lock_change(lock, width, height):
"""Handle aspect ratio lock"""
if lock and width:
# Maintain 16:9 ratio
new_height = width * 9 / 16
return width, new_height
return width, height
def on_process(
video_path, x, y, width, height,
language, start, end, progress_bar
):
"""Handle video processing"""
if not video_path:
return (
gr.update(),
"Please upload a video first.",
"",
None
)
# Get video dimensions
vw, vh, _, _ = get_video_info(video_path)
# Convert percentages to pixels
px = int(x / 100 * vw)
py = int(y / 100 * vh)
pw = int(width / 100 * vw)
ph = int(height / 100 * vh)
subtitles, srt, json_output = process_video_ocr(
video_path, px, py, pw, ph,
language, start, end,
progress_bar
)
if subtitles is None:
return (
gr.update(),
srt, # Error message
"",
None
)
# Format for dataframe
df_data = []
for sub in subtitles:
df_data.append([
sub.get("id", 0),
format_srt_time(sub["start"]).split(",")[0],
format_srt_time(sub["end"]).split(",")[0],
"entered",
sub["text"][:50] + "..." if len(sub["text"]) > 50 else sub["text"]
])
# Create downloadable files
with tempfile.NamedTemporaryFile(mode='w', suffix='.srt', delete=False) as f:
f.write(srt)
srt_path = f.name
with tempfile.NamedTemporaryFile(mode='w', suffix='.txt', delete=False) as f:
txt = generate_txt(subtitles)
f.write(txt)
txt_path = f.name
with tempfile.NamedTemporaryFile(mode='w', suffix='.json', delete=False) as f:
f.write(json_output)
json_path = f.name
return (
df_data,
srt,
{"paths": [srt_path, txt_path, json_path]},
json_output
)
def reset_defaults():
"""Reset all settings to defaults"""
return (
10, # x
80, # y
80, # width
15, # height
"English",
0,
10
)
def update_active_subtitles(subtitle_json, current_time):
"""Update active subtitle status based on current time"""
if not subtitle_json:
return {}
try:
data = json.loads(subtitle_json) if isinstance(subtitle_json, str) else subtitle_json
subtitles = data.get("subtitles", [])
except:
return {}
active = {}
for sub in subtitles:
start = sub.get("start", 0)
end = sub.get("end", 0)
sub_id = sub.get("id", 0)
if current_time < start:
status = "entered"
elif start <= current_time <= end:
status = "active"
else:
status = "exited"
active[f"Subtitle {sub_id}"] = {
"status": status,
"text": sub["text"][:30] + "..." if len(sub["text"]) > 30 else sub["text"],
"time": f"{start:.2f}s - {end:.2f}s"
}
return active
# Bind events
video_input.change(
update_video_info,
inputs=[video_input],
outputs=[video_info, video_info, end_time]
).then(
update_end_time_on_load,
inputs=[video_info, end_time],
outputs=[end_time]
)
aspect_lock.change(
on_aspect_lock_change,
inputs=[aspect_lock, box_width, box_height],
outputs=[box_width, box_height]
)
reset_btn.click(
reset_defaults,
outputs=[x_pos, y_pos, box_width, box_height, language, start_time, end_time]
)
process_btn.click(
on_process,
inputs=[
video_input, x_pos, y_pos, box_width, box_height,
language, start_time, end_time, progress_bar
],
outputs=[subtitle_list, srt_output, processed_data, subtitle_preview]
)
# Setup download buttons
def prepare_downloads(data_state):
if data_state and "paths" in data_state:
paths = data_state["paths"]
return (
paths[0], # srt
paths[1], # txt
paths[2] # json
)
return None, None, None
processed_data.change(
prepare_downloads,
inputs=[processed_data],
outputs=[download_srt, download_txt, download_json]
)
return demo
# Create and launch the demo
if __name__ == "__main__":
demo = create_demo()
# Custom CSS for better UI
custom_css = """
.gradio-container {
max-width: 1400px !important;
margin: auto;
}
.subtitle-active {
background-color: #4CAF50 !important;
color: white !important;
box-shadow: 0 0 10px #4CAF50;
}
.subtitle-entered {
background-color: #2196F3 !important;
color: white !important;
}
.subtitle-exited {
background-color: #9E9E9E !important;
color: white !important;
opacity: 0.6;
}
"""
demo.launch(
theme=gr.themes.Soft(
primary_hue="indigo",
secondary_hue="purple",
neutral_hue="slate",
font=gr.themes.GoogleFont("Inter")
),
css=custom_css,
footer_links=[
{"label": "Built with anycoder", "url": "https://huggingface.co/spaces/akhaliq/anycoder"}
]
)