Spaces:
Sleeping
Sleeping
Update app.py
Browse files
app.py
CHANGED
|
@@ -9,15 +9,15 @@ import numpy as np
|
|
| 9 |
import streamlit as st
|
| 10 |
from rapidocr_onnxruntime import RapidOCR
|
| 11 |
|
| 12 |
-
# 1. CẤU HÌNH TRANG
|
| 13 |
-
st.set_page_config(page_title="OCR Android
|
| 14 |
|
| 15 |
# --- CACHE MODEL ---
|
| 16 |
@st.cache_resource
|
| 17 |
def load_ocr_model():
|
| 18 |
return RapidOCR()
|
| 19 |
|
| 20 |
-
# ---
|
| 21 |
def similar(a, b):
|
| 22 |
return SequenceMatcher(None, a, b).ratio()
|
| 23 |
|
|
@@ -37,25 +37,18 @@ def get_video_info(video_path):
|
|
| 37 |
cap.release()
|
| 38 |
return width, height, fps, total_frames
|
| 39 |
|
| 40 |
-
# --- ENGINE XỬ LÝ (
|
| 41 |
-
def extract_subtitles(video_path, ocr_engine, crop_ratio, frame_skip, conf_thresh, progress_bar, status_text):
|
| 42 |
cap = cv2.VideoCapture(video_path)
|
| 43 |
fps = cap.get(cv2.CAP_PROP_FPS)
|
| 44 |
total_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
y_start = int(orig_h * (1 - crop_ratio))
|
| 48 |
|
| 49 |
subs = []
|
| 50 |
current_sub = None
|
| 51 |
|
| 52 |
-
|
| 53 |
-
if orig_w > 2000:
|
| 54 |
-
resize_scale = 1920 / orig_w
|
| 55 |
-
else:
|
| 56 |
-
resize_scale = 1.0
|
| 57 |
-
|
| 58 |
-
prev_roi_enhanced = None
|
| 59 |
last_text = ""
|
| 60 |
frame_idx = 0
|
| 61 |
pbar_cnt = 0
|
|
@@ -64,6 +57,7 @@ def extract_subtitles(video_path, ocr_engine, crop_ratio, frame_skip, conf_thres
|
|
| 64 |
ret, frame = cap.read()
|
| 65 |
if not ret: break
|
| 66 |
|
|
|
|
| 67 |
if frame_idx % frame_skip != 0:
|
| 68 |
frame_idx += 1
|
| 69 |
continue
|
|
@@ -72,58 +66,78 @@ def extract_subtitles(video_path, ocr_engine, crop_ratio, frame_skip, conf_thres
|
|
| 72 |
if pbar_cnt % 20 == 0:
|
| 73 |
prog = min(frame_idx / total_frames, 1.0)
|
| 74 |
progress_bar.progress(prog)
|
| 75 |
-
|
|
|
|
|
|
|
| 76 |
|
| 77 |
-
|
|
|
|
| 78 |
|
| 79 |
-
|
| 80 |
-
|
| 81 |
-
|
| 82 |
-
roi_gray = cv2.cvtColor(roi, cv2.COLOR_BGR2GRAY)
|
| 83 |
-
roi_enhanced = cv2.normalize(roi_gray, None, 0, 255, cv2.NORM_MINMAX)
|
| 84 |
-
|
| 85 |
should_run_ocr = True
|
| 86 |
|
| 87 |
-
if
|
| 88 |
-
|
| 89 |
-
|
| 90 |
-
|
| 91 |
-
|
| 92 |
-
|
| 93 |
-
|
| 94 |
-
|
| 95 |
-
|
| 96 |
-
|
| 97 |
-
|
|
|
|
|
|
|
| 98 |
if should_run_ocr:
|
| 99 |
-
res, _ = ocr_engine(roi)
|
|
|
|
| 100 |
text = " ".join([line[1] for line in res if float(line[2]) >= conf_thresh]).strip() if res else ""
|
| 101 |
-
last_text = text
|
| 102 |
|
| 103 |
timestamp = frame_idx / fps
|
| 104 |
|
|
|
|
| 105 |
if text:
|
| 106 |
if current_sub is None:
|
| 107 |
current_sub = {'start': timestamp, 'end': timestamp, 'text': text}
|
| 108 |
else:
|
| 109 |
-
|
|
|
|
| 110 |
current_sub['end'] = timestamp
|
| 111 |
-
|
|
|
|
|
|
|
| 112 |
else:
|
| 113 |
-
|
|
|
|
|
|
|
| 114 |
current_sub = {'start': timestamp, 'end': timestamp, 'text': text}
|
| 115 |
else:
|
|
|
|
| 116 |
if current_sub:
|
| 117 |
-
if current_sub['end'] - current_sub['start'] > 0.1:
|
|
|
|
| 118 |
current_sub = None
|
|
|
|
| 119 |
frame_idx += 1
|
| 120 |
|
| 121 |
-
if current_sub and (current_sub['end'] - current_sub['start'] > 0.1):
|
|
|
|
|
|
|
| 122 |
cap.release()
|
| 123 |
|
|
|
|
| 124 |
final_subs = []
|
| 125 |
for i, s in enumerate(subs):
|
| 126 |
-
final_subs.append({
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 127 |
return final_subs
|
| 128 |
|
| 129 |
def generate_srt_content(subs):
|
|
@@ -132,37 +146,36 @@ def generate_srt_content(subs):
|
|
| 132 |
srt_content += f"{sub['index']}\n{sub['start']} --> {sub['end']}\n{sub['text']}\n\n"
|
| 133 |
return srt_content
|
| 134 |
|
| 135 |
-
# --- GIAO DIỆN ANDROID
|
| 136 |
|
| 137 |
-
st.markdown("### 📱 Video OCR (
|
| 138 |
|
| 139 |
-
# Upload file
|
| 140 |
uploaded_file = st.file_uploader("Chọn Video:", type=["mp4", "avi", "mkv"])
|
| 141 |
|
| 142 |
if uploaded_file is not None:
|
| 143 |
-
#
|
| 144 |
tfile = tempfile.NamedTemporaryFile(delete=False, suffix='.mp4')
|
| 145 |
chunk_size = 10 * 1024 * 1024
|
| 146 |
-
with st.
|
| 147 |
while True:
|
| 148 |
chunk = uploaded_file.read(chunk_size)
|
| 149 |
if not chunk: break
|
| 150 |
tfile.write(chunk)
|
| 151 |
-
|
|
|
|
|
|
|
| 152 |
video_path = tfile.name
|
| 153 |
|
| 154 |
try:
|
| 155 |
width, height, fps, total_frames = get_video_info(video_path)
|
| 156 |
-
except:
|
| 157 |
-
width = None
|
| 158 |
|
| 159 |
if width:
|
| 160 |
-
# --- PHẦN ĐIỀU CHỈNH VÙNG QUÉT (DỄ DÙNG CHO MOBILE) ---
|
| 161 |
st.write("---")
|
| 162 |
-
st.write("### 1. Chỉnh Vạch Đỏ (
|
| 163 |
|
| 164 |
-
#
|
| 165 |
-
preview_frame_idx = int(total_frames * 0.2)
|
| 166 |
cap = cv2.VideoCapture(video_path)
|
| 167 |
cap.set(cv2.CAP_PROP_POS_FRAMES, preview_frame_idx)
|
| 168 |
ret, frame = cap.read()
|
|
@@ -171,45 +184,54 @@ if uploaded_file is not None:
|
|
| 171 |
if "crop_val" not in st.session_state:
|
| 172 |
st.session_state.crop_val = 0.30
|
| 173 |
|
| 174 |
-
#
|
| 175 |
c1, c2 = st.columns([1, 1])
|
| 176 |
with c1:
|
| 177 |
-
|
| 178 |
-
crop_ratio = st.number_input("Cao độ vạch đỏ (0.1 - 0.5)",
|
| 179 |
min_value=0.1, max_value=0.6,
|
| 180 |
value=st.session_state.crop_val,
|
| 181 |
-
step=0.01,
|
| 182 |
-
format="%.2f")
|
| 183 |
with c2:
|
| 184 |
-
st.info("
|
| 185 |
|
| 186 |
-
# Hiển thị ảnh ngay bên dưới nút chỉnh
|
| 187 |
if ret:
|
| 188 |
-
# Resize
|
| 189 |
-
display_scale = 400 / width if width > 400 else 1.0
|
| 190 |
small_h = int(height * display_scale)
|
| 191 |
preview_small = cv2.resize(frame, (int(width*display_scale), small_h))
|
| 192 |
|
| 193 |
line_y = int(small_h * (1 - crop_ratio))
|
| 194 |
cv2.line(preview_small, (0, line_y), (preview_small.shape[1], line_y), (0, 0, 255), 2)
|
| 195 |
|
| 196 |
-
st.image(preview_small, channels="BGR", caption="
|
| 197 |
|
| 198 |
st.write("---")
|
| 199 |
-
st.write("### 2. Cấu hình
|
| 200 |
|
| 201 |
-
|
| 202 |
-
|
| 203 |
-
|
| 204 |
-
|
| 205 |
-
|
| 206 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 207 |
try:
|
| 208 |
ocr_engine = load_ocr_model()
|
| 209 |
prog_bar = st.progress(0)
|
| 210 |
status_txt = st.empty()
|
| 211 |
|
| 212 |
-
|
|
|
|
| 213 |
|
| 214 |
prog_bar.progress(100)
|
| 215 |
|
|
@@ -218,6 +240,6 @@ if uploaded_file is not None:
|
|
| 218 |
srt_data = generate_srt_content(subs)
|
| 219 |
st.download_button("📥 TẢI FILE SRT", srt_data, file_name="subtitle.srt", use_container_width=True)
|
| 220 |
else:
|
| 221 |
-
st.error("❌
|
| 222 |
except Exception as e:
|
| 223 |
st.error(f"Lỗi: {e}")
|
|
|
|
| 9 |
import streamlit as st
|
| 10 |
from rapidocr_onnxruntime import RapidOCR
|
| 11 |
|
| 12 |
+
# 1. CẤU HÌNH TRANG MOBILE
|
| 13 |
+
st.set_page_config(page_title="OCR Android: Chậm & Chắc", layout="centered")
|
| 14 |
|
| 15 |
# --- CACHE MODEL ---
|
| 16 |
@st.cache_resource
|
| 17 |
def load_ocr_model():
|
| 18 |
return RapidOCR()
|
| 19 |
|
| 20 |
+
# --- HÀM HỖ TRỢ ---
|
| 21 |
def similar(a, b):
|
| 22 |
return SequenceMatcher(None, a, b).ratio()
|
| 23 |
|
|
|
|
| 37 |
cap.release()
|
| 38 |
return width, height, fps, total_frames
|
| 39 |
|
| 40 |
+
# --- ENGINE XỬ LÝ (ĐÃ TINH CHỈNH ĐỂ BẮT DÍNH MỌI CHỮ) ---
|
| 41 |
+
def extract_subtitles(video_path, ocr_engine, crop_ratio, frame_skip, conf_thresh, use_smart_filter, progress_bar, status_text):
|
| 42 |
cap = cv2.VideoCapture(video_path)
|
| 43 |
fps = cap.get(cv2.CAP_PROP_FPS)
|
| 44 |
total_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
|
| 45 |
+
height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
|
| 46 |
+
y_start = int(height * (1 - crop_ratio))
|
|
|
|
| 47 |
|
| 48 |
subs = []
|
| 49 |
current_sub = None
|
| 50 |
|
| 51 |
+
prev_roi_gray = None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 52 |
last_text = ""
|
| 53 |
frame_idx = 0
|
| 54 |
pbar_cnt = 0
|
|
|
|
| 57 |
ret, frame = cap.read()
|
| 58 |
if not ret: break
|
| 59 |
|
| 60 |
+
# Nhảy frame (Skip)
|
| 61 |
if frame_idx % frame_skip != 0:
|
| 62 |
frame_idx += 1
|
| 63 |
continue
|
|
|
|
| 66 |
if pbar_cnt % 20 == 0:
|
| 67 |
prog = min(frame_idx / total_frames, 1.0)
|
| 68 |
progress_bar.progress(prog)
|
| 69 |
+
# Hiển thị giây hiện tại để biết máy đang chạy đến đâu
|
| 70 |
+
current_sec = int(frame_idx/fps)
|
| 71 |
+
status_text.text(f"🔍 Đang soi kỹ... {int(prog*100)}% (Giây thứ: {current_sec})")
|
| 72 |
|
| 73 |
+
# 1. Cắt vùng sub
|
| 74 |
+
roi = frame[y_start:height, :]
|
| 75 |
|
| 76 |
+
# 2. Xử lý ảnh (Smart Filter)
|
| 77 |
+
# Nếu bật chế độ này, máy sẽ so sánh với frame trước để bỏ qua nếu giống nhau
|
| 78 |
+
# Nếu tắt (False), máy sẽ OCR tất cả các frame -> Chậm nhưng KHÔNG SÓT CHỮ
|
|
|
|
|
|
|
|
|
|
| 79 |
should_run_ocr = True
|
| 80 |
|
| 81 |
+
if use_smart_filter:
|
| 82 |
+
roi_gray = cv2.cvtColor(roi, cv2.COLOR_BGR2GRAY)
|
| 83 |
+
if prev_roi_gray is not None:
|
| 84 |
+
try:
|
| 85 |
+
score = cv2.absdiff(roi_gray, prev_roi_gray)
|
| 86 |
+
non_zero = np.count_nonzero(score > 30)
|
| 87 |
+
if non_zero / roi_gray.size < 0.03: # Nếu thay đổi < 3%
|
| 88 |
+
should_run_ocr = False
|
| 89 |
+
text = last_text
|
| 90 |
+
except: pass
|
| 91 |
+
prev_roi_gray = roi_gray
|
| 92 |
+
|
| 93 |
+
# 3. Chạy OCR
|
| 94 |
if should_run_ocr:
|
| 95 |
+
res, _ = ocr_engine(roi)
|
| 96 |
+
# Lọc tin cậy: Chỉ lấy chữ rõ
|
| 97 |
text = " ".join([line[1] for line in res if float(line[2]) >= conf_thresh]).strip() if res else ""
|
| 98 |
+
last_text = text
|
| 99 |
|
| 100 |
timestamp = frame_idx / fps
|
| 101 |
|
| 102 |
+
# 4. Logic gộp sub (Đã nới lỏng để bắt nhạy hơn)
|
| 103 |
if text:
|
| 104 |
if current_sub is None:
|
| 105 |
current_sub = {'start': timestamp, 'end': timestamp, 'text': text}
|
| 106 |
else:
|
| 107 |
+
# Nếu giống > 70% thì gộp (Giảm từ 75 xuống 70 để đỡ bị cắt vụn)
|
| 108 |
+
if similar(text, current_sub['text']) > 0.70:
|
| 109 |
current_sub['end'] = timestamp
|
| 110 |
+
# Luôn ưu tiên lấy câu dài hơn
|
| 111 |
+
if len(text) > len(current_sub['text']):
|
| 112 |
+
current_sub['text'] = text
|
| 113 |
else:
|
| 114 |
+
# Lưu câu cũ
|
| 115 |
+
if current_sub['end'] - current_sub['start'] > 0.1:
|
| 116 |
+
subs.append(current_sub)
|
| 117 |
current_sub = {'start': timestamp, 'end': timestamp, 'text': text}
|
| 118 |
else:
|
| 119 |
+
# Khoảng trống
|
| 120 |
if current_sub:
|
| 121 |
+
if current_sub['end'] - current_sub['start'] > 0.1:
|
| 122 |
+
subs.append(current_sub)
|
| 123 |
current_sub = None
|
| 124 |
+
|
| 125 |
frame_idx += 1
|
| 126 |
|
| 127 |
+
if current_sub and (current_sub['end'] - current_sub['start'] > 0.1):
|
| 128 |
+
subs.append(current_sub)
|
| 129 |
+
|
| 130 |
cap.release()
|
| 131 |
|
| 132 |
+
# Format kết quả
|
| 133 |
final_subs = []
|
| 134 |
for i, s in enumerate(subs):
|
| 135 |
+
final_subs.append({
|
| 136 |
+
"index": i + 1,
|
| 137 |
+
"start": format_timestamp(s['start']),
|
| 138 |
+
"end": format_timestamp(s['end']),
|
| 139 |
+
"text": s['text']
|
| 140 |
+
})
|
| 141 |
return final_subs
|
| 142 |
|
| 143 |
def generate_srt_content(subs):
|
|
|
|
| 146 |
srt_content += f"{sub['index']}\n{sub['start']} --> {sub['end']}\n{sub['text']}\n\n"
|
| 147 |
return srt_content
|
| 148 |
|
| 149 |
+
# --- GIAO DIỆN ANDROID ---
|
| 150 |
|
| 151 |
+
st.markdown("### 📱 Video OCR (Chậm mà Chắc)")
|
| 152 |
|
|
|
|
| 153 |
uploaded_file = st.file_uploader("Chọn Video:", type=["mp4", "avi", "mkv"])
|
| 154 |
|
| 155 |
if uploaded_file is not None:
|
| 156 |
+
# Lưu file tạm
|
| 157 |
tfile = tempfile.NamedTemporaryFile(delete=False, suffix='.mp4')
|
| 158 |
chunk_size = 10 * 1024 * 1024
|
| 159 |
+
with st.status("Đang chuẩn bị...", expanded=True) as status:
|
| 160 |
while True:
|
| 161 |
chunk = uploaded_file.read(chunk_size)
|
| 162 |
if not chunk: break
|
| 163 |
tfile.write(chunk)
|
| 164 |
+
tfile.close()
|
| 165 |
+
status.update(label="Đã xong!", state="complete", expanded=False)
|
| 166 |
+
|
| 167 |
video_path = tfile.name
|
| 168 |
|
| 169 |
try:
|
| 170 |
width, height, fps, total_frames = get_video_info(video_path)
|
| 171 |
+
except: width = None
|
|
|
|
| 172 |
|
| 173 |
if width:
|
|
|
|
| 174 |
st.write("---")
|
| 175 |
+
st.write("#### 1. Chỉnh Vạch Đỏ (Quan trọng nhất)")
|
| 176 |
|
| 177 |
+
# Preview frame
|
| 178 |
+
preview_frame_idx = int(total_frames * 0.2)
|
| 179 |
cap = cv2.VideoCapture(video_path)
|
| 180 |
cap.set(cv2.CAP_PROP_POS_FRAMES, preview_frame_idx)
|
| 181 |
ret, frame = cap.read()
|
|
|
|
| 184 |
if "crop_val" not in st.session_state:
|
| 185 |
st.session_state.crop_val = 0.30
|
| 186 |
|
| 187 |
+
# Giao diện nút bấm +/-
|
| 188 |
c1, c2 = st.columns([1, 1])
|
| 189 |
with c1:
|
| 190 |
+
crop_ratio = st.number_input("Vị trí vạch đỏ:",
|
|
|
|
| 191 |
min_value=0.1, max_value=0.6,
|
| 192 |
value=st.session_state.crop_val,
|
| 193 |
+
step=0.01, format="%.2f")
|
|
|
|
| 194 |
with c2:
|
| 195 |
+
st.info("Bấm (+) (-) để chỉnh. Vạch đỏ phải nằm **NGAY TRÊN ĐẦU** dòng chữ.")
|
| 196 |
|
|
|
|
| 197 |
if ret:
|
| 198 |
+
# Resize ảnh preview cho vừa điện thoại
|
| 199 |
+
display_scale = 400 / width if width > 400 else 1.0
|
| 200 |
small_h = int(height * display_scale)
|
| 201 |
preview_small = cv2.resize(frame, (int(width*display_scale), small_h))
|
| 202 |
|
| 203 |
line_y = int(small_h * (1 - crop_ratio))
|
| 204 |
cv2.line(preview_small, (0, line_y), (preview_small.shape[1], line_y), (0, 0, 255), 2)
|
| 205 |
|
| 206 |
+
st.image(preview_small, channels="BGR", caption="Ảnh xem trước")
|
| 207 |
|
| 208 |
st.write("---")
|
| 209 |
+
st.write("#### 2. Cấu hình quét")
|
| 210 |
|
| 211 |
+
# --- CẤU HÌNH MỚI CHO NGƯỜI DÙNG BỊ MẤT CHỮ ---
|
| 212 |
+
c3, c4 = st.columns([1, 1])
|
| 213 |
+
with c3:
|
| 214 |
+
# Cho phép chọn tốc độ chậm hơn (2 hoặc 3) để không sót
|
| 215 |
+
frame_skip = st.selectbox("Tốc độ (Skip):", [2, 3, 5, 10], index=1,
|
| 216 |
+
help="Chọn 2 hoặc 3 để quét kỹ từng chút (Lâu hơn nhưng ra đủ chữ).")
|
| 217 |
+
with c4:
|
| 218 |
+
# Mặc định để thấp (0.3) để chữ mờ cũng bắt được
|
| 219 |
+
conf_thresh = st.number_input("Độ nhạy (0.1-1.0):", value=0.3, step=0.1)
|
| 220 |
+
|
| 221 |
+
# Thêm nút tắt bộ lọc thông minh
|
| 222 |
+
use_smart_filter = st.checkbox("⚡ Dùng bộ lọc tăng tốc (Tắt nếu bị mất chữ)", value=False)
|
| 223 |
+
if not use_smart_filter:
|
| 224 |
+
st.caption("🐢 Đang tắt bộ lọc: Máy sẽ quét kỹ từng khung hình (Sẽ lâu hơn nhưng chính xác nhất).")
|
| 225 |
+
|
| 226 |
+
# Nút chạy
|
| 227 |
+
if st.button("🚀 BẮT ĐẦU QUÉT", type="primary", use_container_width=True):
|
| 228 |
try:
|
| 229 |
ocr_engine = load_ocr_model()
|
| 230 |
prog_bar = st.progress(0)
|
| 231 |
status_txt = st.empty()
|
| 232 |
|
| 233 |
+
# Gọi hàm với tham số mới
|
| 234 |
+
subs = extract_subtitles(video_path, ocr_engine, crop_ratio, frame_skip, conf_thresh, use_smart_filter, prog_bar, status_txt)
|
| 235 |
|
| 236 |
prog_bar.progress(100)
|
| 237 |
|
|
|
|
| 240 |
srt_data = generate_srt_content(subs)
|
| 241 |
st.download_button("📥 TẢI FILE SRT", srt_data, file_name="subtitle.srt", use_container_width=True)
|
| 242 |
else:
|
| 243 |
+
st.error("❌ Vẫn không thấy chữ. Hãy thử giảm 'Độ nhạy' xuống 0.2")
|
| 244 |
except Exception as e:
|
| 245 |
st.error(f"Lỗi: {e}")
|