hoanglinhn0 commited on
Commit
01dfd90
·
verified ·
1 Parent(s): 5aa6225

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +95 -73
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 (Giao diện mobile gọn gàng)
13
- st.set_page_config(page_title="OCR Android Mobile", layout="centered")
14
 
15
  # --- CACHE MODEL ---
16
  @st.cache_resource
17
  def load_ocr_model():
18
  return RapidOCR()
19
 
20
- # --- CÁC HÀM HỖ TRỢ ---
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Ý (GIỮ NGUYÊN THUẬT TOÁN TỐI ƯU) ---
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
- orig_w = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))
46
- orig_h = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
47
- y_start = int(orig_h * (1 - crop_ratio))
48
 
49
  subs = []
50
  current_sub = None
51
 
52
- # Tự động resize nếu video 4K
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
- status_text.text(f"⏳ Đang chạy... {int(prog*100)}%")
 
 
76
 
77
- roi = frame[y_start:orig_h, :]
 
78
 
79
- if resize_scale < 1.0:
80
- roi = cv2.resize(roi, (0, 0), fx=resize_scale, fy=resize_scale)
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 prev_roi_enhanced is not None:
88
- try:
89
- diff = cv2.absdiff(roi_enhanced, prev_roi_enhanced)
90
- non_zero_count = np.count_nonzero(diff > 30)
91
- if non_zero_count / roi_enhanced.size < 0.05:
92
- should_run_ocr = False
93
- text = last_text
94
- except: pass
95
-
96
- prev_roi_enhanced = roi_enhanced
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
- if similar(text, current_sub['text']) > 0.75:
 
110
  current_sub['end'] = timestamp
111
- if len(text) > len(current_sub['text']): current_sub['text'] = text
 
 
112
  else:
113
- if current_sub['end'] - current_sub['start'] > 0.1: subs.append(current_sub)
 
 
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: subs.append(current_sub)
 
118
  current_sub = None
 
119
  frame_idx += 1
120
 
121
- if current_sub and (current_sub['end'] - current_sub['start'] > 0.1): subs.append(current_sub)
 
 
122
  cap.release()
123
 
 
124
  final_subs = []
125
  for i, s in enumerate(subs):
126
- final_subs.append({"index": i + 1, "start": format_timestamp(s['start']), "end": format_timestamp(s['end']), "text": s['text']})
 
 
 
 
 
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 TỐI GIẢN ---
136
 
137
- st.markdown("### 📱 Video OCR (Android Mode)")
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
- # Chunking save
144
  tfile = tempfile.NamedTemporaryFile(delete=False, suffix='.mp4')
145
  chunk_size = 10 * 1024 * 1024
146
- with st.spinner("Đang tải video..."):
147
  while True:
148
  chunk = uploaded_file.read(chunk_size)
149
  if not chunk: break
150
  tfile.write(chunk)
151
- tfile.close()
 
 
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 Đỏ (Vùng quét)")
163
 
164
- # Xem trước ảnh
165
- preview_frame_idx = int(total_frames * 0.2) # Mặc định lấy frame ở 20% video
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
- # Cột chia đôi để nút bấm to hơn
175
  c1, c2 = st.columns([1, 1])
176
  with c1:
177
- # Thay Slider bằng Number Input ( nút + - dễ bấm trên Android)
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, # Bước nhảy nhỏ để chỉnh tinh
182
- format="%.2f")
183
  with c2:
184
- st.info("💡 Bấm dấu (+) (-) để nhích vạch đỏ lên xuống.")
185
 
186
- # Hiển thị ảnh ngay bên dưới nút chỉnh
187
  if ret:
188
- # Resize để vừa màn hình điện thoại
189
- display_scale = 400 / width if width > 400 else 1.0 # 400px là vừa ngang đt
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="Vạch đỏ phải nằm TRÊN đầu chữ một chút")
197
 
198
  st.write("---")
199
- st.write("### 2. Cấu hình & Chạy")
200
 
201
- with st.expander("⚙️ Cài đặt nâng cao (Bấm để mở)"):
202
- frame_skip = st.selectbox("Tốc độ quét:", [5, 10, 15], index=1, help="Số lớn chạy nhanh hơn.")
203
- conf_thresh = st.slider("Độ nhạy (Tin cậy):", 0.1, 1.0, 0.40)
204
-
205
- # Nút chạy to
206
- if st.button("🚀 BẮT ĐẦU QUÉT NGAY", type="primary", use_container_width=True):
 
 
 
 
 
 
 
 
 
 
 
207
  try:
208
  ocr_engine = load_ocr_model()
209
  prog_bar = st.progress(0)
210
  status_txt = st.empty()
211
 
212
- subs = extract_subtitles(video_path, ocr_engine, crop_ratio, frame_skip, conf_thresh, prog_bar, status_txt)
 
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("❌ Không thấy chữ! Hãy nhích vạch đỏ thấp xuống hoặc cao lên chút nữa.")
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
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}")