# 修改后的主处理流程 import json import os import base64 import argparse from collections import defaultdict, Counter from datetime import timedelta from tool.Ocr.tencentcloud_ocr import tencentcloud_ocr_api, tencent_ocr_video_all_frames, img2base64 from difflib import SequenceMatcher import re import random import cv2 import jieba from collections import deque from concurrent.futures import ThreadPoolExecutor, as_completed from config.config_utils import config import numpy as np from sklearn.cluster import DBSCAN import math def _jpeg_size(path): """只读 JPEG 文件头拿到 (宽,高), 不解码全图、不依赖 PIL。失败返回 None。""" try: with open(path, 'rb') as f: if f.read(2) != b'\xff\xd8': # 不是 JPEG return None while True: b = f.read(1) while b and b != b'\xff': # 找到下一个标记起始 0xFF b = f.read(1) marker = f.read(1) while marker == b'\xff': # 跳过填充 0xFF marker = f.read(1) if not marker: return None m = marker[0] # SOF 标记(含尺寸): C0-CF, 排除 C4(DHT) C8(JPG) CC(DAC) if 0xC0 <= m <= 0xCF and m not in (0xC4, 0xC8, 0xCC): f.read(3) # 段长(2) + 精度(1) h = int.from_bytes(f.read(2), 'big') w = int.from_bytes(f.read(2), 'big') return w, h seg = int.from_bytes(f.read(2), 'big') # 其它段: 跳过 if seg < 2: return None f.seek(seg - 2, 1) except Exception: return None def _img2base64_capped(img_path, max_side): """给OCR的图片加【像素尺寸上限】: 最长边超过 max_side 就等比缩小再送。 防止区域检测过高导致的超大合成图(几千×上万像素)让单张OCR耗时>30s超时。 纯Python读图头判尺寸(无需PIL), 只有超限的才用 cv2 解码+缩放; 正常图原样送。""" try: if max_side and max_side > 0: sz = _jpeg_size(img_path) if sz and max(sz) > max_side: im = cv2.imread(img_path) if im is not None: h, w = im.shape[:2] sc = max_side / float(max(h, w)) im = cv2.resize(im, (max(1, int(w * sc)), max(1, int(h * sc))), interpolation=cv2.INTER_AREA) ok, buf = cv2.imencode('.jpg', im, [int(cv2.IMWRITE_JPEG_QUALITY), 90]) if ok: return base64.b64encode(buf.tobytes()).decode() except Exception: pass return img2base64(img_path=img_path) def _safe_ocr_image(img_path): """对单张图片做 OCR,捕获所有异常,返回 (原始字符串结果, 错误信息或None)。 供并发 OCR 的 worker 调用——不直接写日志(worker运行在子线程,logger为线程本地, 日志会丢失),错误信息回传给主线程统一记录。任何一张图失败都不影响其他图。""" empty = json.dumps({"TextDetections": [], "Language": "zh", "RequestId": "ocr_error"}, ensure_ascii=False) try: try: _max_side = int(config.get_subtitle_config().get('ocr_max_image_side', 4000) or 0) except (ValueError, TypeError, AttributeError): _max_side = 4000 img = _img2base64_capped(img_path, _max_side) result = tencentcloud_ocr_api(image_base64=img) if not result: return empty, f"OCR返回空结果: {img_path}" return result, None except Exception as e: return empty, f"OCR调用异常 {img_path}: {e}" import time _EMPTY_OCR = json.dumps({"TextDetections": [], "Language": "zh", "RequestId": "ocr_error"}, ensure_ascii=False) class OcrIncompleteError(Exception): """多轮重试后仍有图 OCR 失败。为"不丢帧": 不产出带洞的SRT, 让整段视频判失败去重跑。""" pass def _is_failed_ocr(raw): """判断某OCR结果是不是"失败"(全端点超时/异常)。 注意: TextDetections 为空但 RequestId 不是 ocr_error → 是合法的"图里确实没字", 不算失败, 不重试。""" if not raw: return True try: return json.loads(raw).get("RequestId") == "ocr_error" except (json.JSONDecodeError, TypeError): return True def _ocr_pass(img_paths, workers): """对一组图并发 OCR 一遍, 返回 ({path: raw}, [错误信息])。单张失败返回空结构, 不抛异常。""" results, errors = {}, [] with ThreadPoolExecutor(max_workers=workers) as executor: future_to_path = {executor.submit(_safe_ocr_image, p): p for p in img_paths} for future in as_completed(future_to_path): path = future_to_path[future] try: ocr_raw, err = future.result() results[path] = ocr_raw if err: errors.append(err) except Exception as e: # _safe_ocr_image 已兜底, 这里双保险 errors.append(f"OCR任务异常 {path}: {e}") results[path] = _EMPTY_OCR return results, errors def _batch_ocr_images(img_paths, logger=None): """并发对一组图片做 OCR,返回 {img_path: ocr_raw_string}。 【不丢帧】: 失败(全端点超时)的图【多轮重试】直到全部成功; 失败多是瞬时超时, 多轮基本都能救回。 若 ocr_no_drop=true(默认), 多轮后仍有失败 → 抛 OcrIncompleteError → 整段视频判失败去重跑, 绝不产出"带洞的SRT"标成功。""" results = {} if not img_paths: return results sub = config.get_subtitle_config() try: workers = int(sub.get('ocr_workers', 10)) except (ValueError, TypeError): workers = 10 workers = max(1, min(workers, len(img_paths))) try: max_passes = max(1, int(sub.get('ocr_max_passes', 5) or 5)) except (ValueError, TypeError): max_passes = 5 try: sleep_s = max(0.0, float(sub.get('ocr_retry_sleep', 2) or 2)) except (ValueError, TypeError): sleep_s = 2.0 no_drop = bool(sub.get('ocr_no_drop', True)) total = len(img_paths) results, errors = _ocr_pass(img_paths, workers) # —— 多轮重试: 只重试还失败的, 直到全部成功或用满轮数 —— # 等待时间随轮数线性递增(sleep_s*轮数), 而不是固定间隔: 失败多半是端点拥堵/冷却, # 固定短间隔等于一直硬撞同一批还没缓过来的端点; 递增给拥堵端点更多恢复时间。 pass_no = 1 while pass_no < max_passes: failed = [p for p in img_paths if _is_failed_ocr(results.get(p))] if not failed: break wait_s = sleep_s * pass_no if logger: logger.log(f"OCR第{pass_no}轮后仍{len(failed)}张失败, {wait_s}s后重试...", level="warning", program="getsrt") if wait_s: time.sleep(wait_s) retry_results, _ = _ocr_pass(failed, max(1, min(workers, len(failed)))) for p, raw in retry_results.items(): if not _is_failed_ocr(raw): results[p] = raw pass_no += 1 failed = [p for p in img_paths if _is_failed_ocr(results.get(p))] if logger: for msg in errors[:50]: logger.log(msg, level="warning", program="getsrt") logger.log(f"OCR完成: 共{total}张, {pass_no}轮后最终失败{len(failed)}张", program="getsrt") # —— 不丢帧: 仍有失败就别产出带洞SRT, 抛异常让整段视频判失败去重跑 —— if failed and no_drop: raise OcrIncompleteError( f"OCR多轮({max_passes})后仍有 {len(failed)}/{total} 张失败, 为不丢帧放弃本视频(待重跑)") return results def calculate_similarity(text1, text2): """计算两个文本的相似度""" if not text1 or not text2: return 0.0 return SequenceMatcher(None, text1, text2).ratio() def process_composite_ocr_to_jsonl(metadata_path, composite_dir, output_jsonl_path,logger): """ 处理拼接图片的OCR结果,生成与tencent_ocr_video_all_frames相同格式的jsonl文件 参数: metadata_path: metadata.json文件路径 composite_dir: 拼接图片所在目录 output_jsonl_path: 输出jsonl文件路径 """ # 读取metadata.json with open(metadata_path, 'r', encoding='utf-8') as f: metadata = json.load(f) # 获取所有原始帧的路径(包括kept_first和kept_last) all_frames = [] original_dir = os.path.dirname(metadata_path) # 假设原始帧与metadata在同一目录 # 添加kept_first帧 for frame_name in metadata['kept_first']: all_frames.append({ 'source_image': frame_name, 'is_blank': False, 'composite_source': None }) # 添加composite中的帧 for composite in metadata['composites']: composite_id = composite['composite_id'] composite_file = composite['file_name'] composite_path = os.path.join(composite_dir, composite_file) for tile in composite['tiles']: if tile['is_blank']: continue else: all_frames.append({ 'source_image': tile['source_image'], 'is_blank': False, 'composite_source': { 'composite_path': composite_path, 'x': tile['x'], 'y': tile['y'], 'width': tile['width'], 'height': tile['height'] } }) # 添加kept_last帧 for frame_name in metadata['kept_last']: all_frames.append({ 'source_image': frame_name, 'is_blank': False, 'composite_source': None }) # 对每个拼接图只进行一次OCR(并发) composite_paths = [ os.path.join(composite_dir, c['file_name']) for c in metadata['composites'] ] logger.log(f"开始并发OCR {len(composite_paths)} 张合成图", program="getsrt") logger.set_stat("composite_ocr_count", len(composite_paths)) composite_ocr_results = _batch_ocr_images(composite_paths, logger) # 预先并发OCR所有需要单独处理的帧(kept_first / kept_last,去重且只取存在的文件) single_frame_paths = [] seen_single = set() for frame_info in all_frames: if frame_info['is_blank'] or frame_info['composite_source']: continue fp = os.path.join(original_dir, frame_info['source_image']) if fp in seen_single: continue seen_single.add(fp) if os.path.exists(fp): single_frame_paths.append(fp) if single_frame_paths: logger.log(f"开始并发OCR {len(single_frame_paths)} 张单独帧", program="getsrt") logger.set_stat("single_frame_ocr_count", len(single_frame_paths)) logger.set_stat("total_ocr_count", len(composite_paths) + len(single_frame_paths)) single_ocr_results = _batch_ocr_images(single_frame_paths, logger) blank = 0 # 生成jsonl文件 with open(output_jsonl_path, 'w', encoding='utf-8') as f: for frame_info in all_frames: frame_name = frame_info['source_image'] frame_path = os.path.join(original_dir, frame_name) # 写入图片路径 f.write(f"pic_path: {frame_path}\n") # 生成OCR结果 if frame_info['is_blank']: if frame_info['composite_source']: blank=blank+1 continue elif frame_info['composite_source']: # 从拼接图片的OCR结果中提取属于该区域的文字框 composite_info = frame_info['composite_source'] composite_ocr = composite_ocr_results[composite_info['composite_path']] try: composite_ocr = json.loads(composite_ocr) except (json.JSONDecodeError, TypeError) as e: logger.log(f"警告: 无法解析拼接图OCR结果: {e}",level="warning",program="getsrt") composite_ocr = {"TextDetections": [], "Language": "zh", "RequestId": "parse_error"} # 提取属于该区域的文字框 region_ocr = extract_ocr_for_region( composite_ocr, composite_info['x'], composite_info['y'], composite_info['width'], composite_info['height'] ) ocr_result = region_ocr else: # 单独处理的帧(kept_first和kept_last),从预先并发OCR的结果中读取 if frame_path not in single_ocr_results: logger.log(f"跳过已清理的帧: {frame_path}", level="warning", program="getsrt") continue ocr_result = single_ocr_results[frame_path] try: ocr_result = json.loads(ocr_result) except (json.JSONDecodeError, TypeError) as e: logger.log(f"警告: 无法解析单独帧OCR结果: {e}",level="warning",program="getsrt") ocr_result = {"TextDetections": [], "Language": "zh", "RequestId": "parse_error"} # 写入OCR结果 f.write(json.dumps(ocr_result, ensure_ascii=False) + "\n") if blank>(len(all_frames)-18)/2: return False logger.log(f"已生成jsonl文件: {output_jsonl_path}",program="getsrt") return True def extract_ocr_for_region(composite_ocr, x, y, width, height): """ 从拼接图的OCR结果中提取属于指定区域的文字框 参数: composite_ocr: 拼接图的OCR结果 x, y: 区域左上角坐标 width, height: 区域宽高 返回: 该区域的OCR结果 """ region_ocr = { "TextDetections": [], } # 计算区域的右下角坐标 region_right = x + width region_bottom = y + height for detection in composite_ocr["TextDetections"]: # 获取文字框的边界 if "Polygon" not in detection: continue polygon = detection["Polygon"] if not polygon: continue # 计算文字框的中心点 xs = [] ys = [] for point in polygon: if "X" in point: xs.append(point["X"]) if "Y" in point: ys.append(point["Y"]) if not xs or not ys: continue center_x = sum(xs) / len(xs) center_y = sum(ys) / len(ys) # 检查中心点是否在区域内 if (x <= center_x <= region_right and y <= center_y <= region_bottom): # 调整坐标,使其相对于区域 adjusted_detection = detection.copy() adjusted_polygon = [] for point in polygon: new_point = {} if "X" in point: new_point["X"] = point["X"] - x if "Y" in point: new_point["Y"] = point["Y"] - y adjusted_polygon.append(new_point) adjusted_detection["Polygon"] = adjusted_polygon region_ocr["TextDetections"].append(adjusted_detection) return region_ocr def analyze_subtitle_blocks(all_pic_ocr_pair, metadata_path,logger): """ 分析所有图片,找到最可能是字幕的区块(10×100网格) 分别处理kept_first/kept_last和composite部分 """ # 读取metadata.json获取帧的分类信息 with open(metadata_path, 'r', encoding='utf-8') as f: metadata = json.load(f) # 分离不同类型的帧 kept_first_names = set(metadata['kept_first']) kept_last_names = set(metadata['kept_last']) kept_first_ocr = [] composite_ocr = [] kept_last_ocr = [] for item in all_pic_ocr_pair: pic_path, ocr_json = item frame_name = os.path.basename(pic_path) if frame_name in kept_first_names: kept_first_ocr.append(item) elif frame_name in kept_last_names: kept_last_ocr.append(item) else: composite_ocr.append(item) # 分别分析kept_first/composite/kept_last的最佳字幕位置 kept_first_grid = analyze_subtitle_blocks_for_group(kept_first_ocr, "kept_first",logger) composite_grid = analyze_subtitle_blocks_for_group(composite_ocr, "composite",logger) kept_last_grid = analyze_subtitle_blocks_for_group(kept_last_ocr, "kept_last",logger) # 返回三个组的分析结果 return { "kept_first": kept_first_grid, "composite": composite_grid, "kept_last": kept_last_grid } def calculate_grouped_similarity_simple(texts,grouped = True): """ 简化的分组相似度计算方法 """ if len(texts) < 2: return 0 # 计算所有文本对之间的相似度 similarities = [] for i in range(len(texts)): for j in range(i+1, len(texts)): if texts[i]==None or texts[j]==None: continue similarity = SequenceMatcher(None, texts[i], texts[j]).ratio() similarities.append(similarity) # 如果有很多高相似度的对,即使有一些低相似度的对,也认为是高相似度 high_similarity_count = sum(1 for s in similarities if s > 0.7) total_pairs = len(texts)*(len(texts)-1)/2 if not grouped: if high_similarity_count / total_pairs > 0.7: # 返回高相似度对的平均相似度 high_similarities = [s for s in similarities if s > 0.7] return sum(high_similarities) / len(high_similarities) else: # 返回所有对的平均相似度 return sum(similarities) / total_pairs # 如果超过一小部分的文本对有高相似度 if high_similarity_count / total_pairs > 0.2: # 返回高相似度对的平均相似度 high_similarities = [s for s in similarities if s > 0.7] return sum(high_similarities) / len(high_similarities) else: # 返回所有对的平均相似度 return sum(similarities) / total_pairs def getaverageh(ocr_group): h=0 tot=0 for item in ocr_group: _, ocr_json = item for text_detection in ocr_json['TextDetections']: if text_detection['Confidence']<=60: continue if 'is_watermark' in text_detection: if text_detection['is_watermark']: continue polygon = [[int(coord['X']), int(coord['Y'])] for coord in text_detection['Polygon']] min_x = min(p[0] for p in polygon) max_x = max(p[0] for p in polygon) max_y = max(p[1] for p in polygon) min_y = min(p[1] for p in polygon) h =h+max_y-min_y tot+=1 if tot==0: return 0 if (tot>3*len(ocr_group)): return 0 h = int(h/tot) return h def analyze_h(all_pic_ocr_pair, metadata_path): """ 分析所有图片,找到最可能是字幕的区块(10×100网格) 分别处理kept_first/kept_last和composite部分 """ # 读取metadata.json获取帧的分类信息 with open(metadata_path, 'r', encoding='utf-8') as f: metadata = json.load(f) # 分离不同类型的帧 kept_first_names = set(metadata['kept_first']) kept_last_names = set(metadata['kept_last']) kept_first_ocr = [] composite_ocr = [] kept_last_ocr = [] for item in all_pic_ocr_pair: pic_path, ocr_json = item frame_name = os.path.basename(pic_path) if frame_name in kept_first_names: kept_first_ocr.append(item) elif frame_name in kept_last_names: kept_last_ocr.append(item) else: composite_ocr.append(item) # 分别分析kept_first/composite/kept_last的最佳字幕位置 kept_first_h = getaverageh(kept_first_ocr) composite_h = getaverageh(composite_ocr) kept_last_h = getaverageh(kept_last_ocr) # 返回三个组的分析结果 return { "kept_first": kept_first_h, "composite": composite_h, "kept_last": kept_last_h } def find_best_cluster_center(candidate_points): """ 找到计数最高的网格点集群的中心 """ # 使用DBSCAN聚类 points = np.array(candidate_points) dbscan = DBSCAN(eps=1.5, min_samples=1) # 宽松参数确保所有点都能聚类 labels = dbscan.fit_predict(points) # 找到最大的簇 clusters = {} for i, label in enumerate(labels): if label not in clusters: clusters[label] = [] clusters[label].append(tuple(points[i])) # 找出最大的簇 max_cluster = max(clusters.values(), key=len) # 计算簇的中心(质心) cluster_array = np.array(max_cluster) center_x = np.mean(cluster_array[:, 0]) center_y = np.mean(cluster_array[:, 1]) return (round(center_x), round(center_y)) def analyze_subtitle_blocks_for_group(ocr_group, group_name,logger): """ 分析特定组的图片,找到最可能是字幕的区块 改进版:位置检测使用所有帧,文字相似度检测使用抽样帧 """ if not ocr_group: print(f"{group_name}组没有OCR数据") return None # 网格划分参数 grid_rows = 50 # 纵向分30块 grid_cols = 200 # 横向分300块 global_left = float('inf') global_right = 0 global_bottom = 0 global_top = float('inf') if group_name=="composite": grid_rows = 50 grid_cols = 100 # 计算全局边界(使用所有帧) for item in ocr_group: _, ocr_json = item for text_detection in ocr_json['TextDetections']: if text_detection['Confidence']<=60: continue polygon = [[int(coord['X']), int(coord['Y'])] for coord in text_detection['Polygon']] min_x = min(p[0] for p in polygon) max_x = max(p[0] for p in polygon) max_y = max(p[1] for p in polygon) min_y = min(p[1] for p in polygon) if min_x < global_left: global_left = min_x if max_x > global_right: global_right = max_x if max_y > global_bottom: global_bottom = max_y if min_y < global_top: global_top = min_y # 设置固定边界 grid_left_bound = global_left grid_right_bound = global_right grid_bottom_bound = global_bottom grid_top_bound = global_top print(f"{group_name}组边界: bottom={global_bottom}, right={global_right}, left={global_left}, top={global_top}") # 计算网格实际宽高 grid_width = (grid_right_bound - grid_left_bound) / grid_cols grid_height = (grid_bottom_bound - grid_top_bound) / grid_rows # 统计网格被包含次数(使用所有帧) grid_counts = defaultdict(int) left = 0 right = 0 middle = 0 for item in ocr_group: _, ocr_json = item for text_detection in ocr_json['TextDetections']: polygon = [[int(coord['X']), int(coord['Y'])] for coord in text_detection['Polygon']] min_x = min(p[0] for p in polygon) max_x = max(p[0] for p in polygon) min_y = min(p[1] for p in polygon) max_y = max(p[1] for p in polygon) if len(text_detection['DetectedText'])<5: continue if text_detection['Confidence']<=60: continue # 检查每个网格 for row in range(grid_rows): for col in range(grid_cols): cell_left = grid_left_bound + col * grid_width cell_right = cell_left + grid_width cell_top = grid_top_bound + row * grid_height cell_bottom = cell_top + grid_height # 检查网格中心是否在文字框内 center_x = (cell_left + cell_right) / 2 center_y = (cell_top + cell_bottom) / 2 if (min_x <= center_x <= max_x) and (min_y <= center_y <= max_y): grid_counts[(row, col)] += 1 # 从所有帧中均匀抽样10帧用于文字相似度检测 sample_size = min(10, len(ocr_group)) if len(ocr_group) <= sample_size: sample_frames = ocr_group else: # 计算抽样间隔 step = len(ocr_group) // sample_size # 从不同位置开始抽样,确保覆盖整个视频 start_pos = min(10,len(ocr_group)-sample_size)+ random.randint(0, step - 1) sample_frames = [ocr_group[i] for i in range(start_pos, len(ocr_group), step)][:sample_size] print(f"从{group_name}组中抽样{sample_size}帧进行文字相似度分析") # 统计抽样帧中每个网格的文字内容 grid_texts = defaultdict(list) # 存储每个网格的文字内容 for item in sample_frames: _, ocr_json = item for text_detection in ocr_json['TextDetections']: polygon = [[int(coord['X']), int(coord['Y'])] for coord in text_detection['Polygon']] min_x = min(p[0] for p in polygon) max_x = max(p[0] for p in polygon) min_y = min(p[1] for p in polygon) max_y = max(p[1] for p in polygon) # 检查每个网格 for row in range(grid_rows): for col in range(grid_cols): cell_left = grid_left_bound + col * grid_width cell_right = cell_left + grid_width cell_top = grid_top_bound + row * grid_height cell_bottom = cell_top + grid_height # 检查网格中心是否在文字框内 center_x = (cell_left + cell_right) / 2 center_y = (cell_top + cell_bottom) / 2 if (min_x <= center_x <= max_x) and (min_y <= center_y <= max_y): if 'DetectedText' in text_detection and text_detection['Confidence']>=50: grid_texts[(row, col)].append(text_detection['DetectedText']) # 计算每个网格的文字相似度(仅对计数较高的网格) adjusted_grid_counts = {} watermark_cells=[] for grid, texts in grid_texts.items(): if group_name=="composite": similarity = calculate_grouped_similarity_simple(texts) else: similarity = calculate_grouped_similarity_simple(texts,grouped = False) # 如果相似度超过阈值,可能是水印,降低权重 if similarity > 0.7: # 相似度阈值,可调整 adjusted_count = 1 # 相似度越高,权重越低 (row,col)=grid cell_left = grid_left_bound + col * grid_width cell_right = cell_left + grid_width cell_top = grid_top_bound + row * grid_height cell_bottom = cell_top + grid_height watermark_cells.append({ 'grid': (row, col), 'center_x': (cell_left + cell_right) / 2, 'center_y': (cell_top + cell_bottom) / 2 }) else: adjusted_count = grid_counts[grid] adjusted_grid_counts[grid] = adjusted_count # 选择最佳网格 if not adjusted_grid_counts: logger.log(f"{group_name}组未找到合适的字幕区域",level="warning",program="getsrt") return None max_count = max(adjusted_grid_counts.values()) print(f"最高调整计数: {max_count}") # 获取最佳网格候选 best_grids = [grid for grid, count in adjusted_grid_counts.items() if count == max_count] # 如果没有找到合适的网格,尝试使用原始计数 if group_name=="composite" and max_count < 2: # 阈值可调整 print("调整后计数太低,尝试使用原始计数") max_count_original = max(grid_counts.values()) best_grids = [grid for grid, count in grid_counts.items() if count == max_count_original] print(f"最高原始计数: {max_count_original}") if not best_grids: logger.log(f"{group_name}组未找到合适的字幕区域",level="warning",program="getsrt") return None if len(best_grids) > 1: # 直接使用中心位置作为最佳网格 best_grid = find_best_cluster_center(best_grids) logger.log(f"{group_name}组发现 {len(best_grids)} 个最佳网格,选择平均网格: {best_grid}",program="getsrt") else: best_grid = best_grids[0] logger.log(f"{group_name}组最佳网格: {best_grid}",program="getsrt") watermark_count=0 for frame_idx, item in enumerate(sample_frames): pic_path, ocr_json = item # 为每个text_detection添加水印标记 for text_detection in ocr_json['TextDetections']: # 初始化水印标记 if 'is_watermark' not in text_detection: text_detection['is_watermark'] = False text_detection['watermark_grid'] = None # 获取文本区域信息 if 'Polygon' not in text_detection: continue polygon = [[int(coord['X']), int(coord['Y'])] for coord in text_detection['Polygon']] min_x = min(p[0] for p in polygon) max_x = max(p[0] for p in polygon) min_y = min(p[1] for p in polygon) max_y = max(p[1] for p in polygon) size = int((max_x-min_x)/grid_width) * int((max_y-min_y)/grid_height) detected_text = text_detection.get('DetectedText', '') confidence = text_detection.get('Confidence', 0) if not detected_text or confidence < 50: continue cell_count = 0 # 检查文本是否在任何一个水印网格中 for cell in watermark_cells: # 检查网格中心是否在文字框内 if (min_x <= cell['center_x'] <= max_x) and (min_y <= cell['center_y'] <= max_y): # 直接修改text_detection标记为水印 cell_count +=1 if cell_count > size*0.5: text_detection['is_watermark'] = True text_detection['watermark_grid'] = cell['grid'] watermark_count += 1 print(f"已标记 {watermark_count} 个水印字幕") return best_grid, grid_width, grid_height, grid_top_bound, grid_left_bound def find_central_subtitles(ocr_json, grid_left, grid_right, grid_top, grid_bottom,frame_name): """ 检测画面中央附近的字幕作为标题 适用于kept_first和kept_last组 """ central_subtitles = [] nearest_subtitles = [] img = cv2.imread(frame_name) height, width = img.shape[:2] min_distance = float('inf') for text_detection in ocr_json['TextDetections']: if text_detection['Confidence'] <= 20: continue polygon = [[int(coord['X']), int(coord['Y'])] for coord in text_detection['Polygon']] min_x = min(p[0] for p in polygon) max_x = max(p[0] for p in polygon) min_y = min(p[1] for p in polygon) max_y = max(p[1] for p in polygon) # 计算文字框中心坐标 box_center_x = (min_x + max_x) / 2 box_center_y = (min_y + max_y) / 2 # 检查是否在画面中央区域 # 中央区域定义为:水平方向在中间50%,垂直方向在中间40%-60% screen_center_x = width/ 2 screen_center_y = height / 2 # 允许一定的偏移范围 horizontal_tolerance = width * 0.1 # 水平方向40%的容差 vertical_tolerance = height * 0.1 # 垂直方向30%的容差 distance = ((box_center_x - screen_center_x) ** 2 + (box_center_y - screen_center_y) ** 2) ** 0.5 # 检查是否在中央区域附近 if (abs(box_center_x - screen_center_x) <= horizontal_tolerance and abs(box_center_y - screen_center_y) <= vertical_tolerance): if 'is_watermark' not in text_detection or not text_detection['is_watermark']: central_subtitles.append({ 'min_y': min_y, 'max_y': max_y, 'text': text_detection['DetectedText'], 'confidence': text_detection['Confidence'] }) subtitle_info = { 'min_y': min_y, 'max_y': max_y, 'text': text_detection['DetectedText'], 'confidence': text_detection['Confidence'], 'distance': distance } # 更新最近的字幕 if distance < min_distance: min_distance = distance nearest_subtitles = [subtitle_info] # 重置为新的最近字幕 elif distance == min_distance: nearest_subtitles.append(subtitle_info) # 添加相同距离的字幕 # 优先返回中央区域内的字幕 if central_subtitles: return central_subtitles # 如果没有中央区域内的字幕,返回离中央最近的字幕 if nearest_subtitles: print(f"未找到中央区域内的字幕,使用离中央最近的字幕(距离: {min_distance:.2f})") return nearest_subtitles return central_subtitles def evaluate_text_quality_rules(text): """ 基于规则评估文本质量 返回一个质量评分(0-1) """ # 1. 计算中文字符比例 total_chars = len(text) chinese_chars = len(re.findall(r'[\u4e00-\u9fff]', text)) chinese_ratio = chinese_chars / total_chars if total_chars > 0 else 0 # 2. 计算中英文混杂程度 # 统计连续中文字符和连续英文字符的数量 chinese_segments = len(re.findall(r'[\u4e00-\u9fff]+', text)) english_segments = len(re.findall(r'[a-zA-Z]+', text)) mixed_score = 1 - min(1, abs(chinese_segments - english_segments) / max(1, chinese_segments + english_segments)) # 3. 计算词语通顺度(使用jieba分词) words = list(jieba.cut(text)) valid_words = [word for word in words if len(word) > 1] # 过滤掉单字 word_score = len(valid_words) / len(words) if words else 0 # 4. 计算特殊字符比例 special_chars = len(re.findall(r'[^a-zA-Z0-9\u4e00-\u9fff\s]', text)) special_ratio = 1 - min(1, special_chars / total_chars) if total_chars > 0 else 1 # 综合评分 quality_score = 0.2 * chinese_ratio + 0.7 * word_score + 0.1 * special_ratio return quality_score from pypinyin import pinyin, Style import re import zhconv # 新增导入 def text_to_pinyin(text): """ 将文本转换为拼音(忽略标点符号和空格) """ # 去除标点符号和空格,只保留中文和英文 cleaned_text = re.sub(r'[^\w\u4e00-\u9fff]', '', text) if not cleaned_text: return "" cleaned_text = zhconv.convert(cleaned_text, 'zh-hans') # 中文转拼音 pinyin_list = [] for char in cleaned_text: if '\u4e00' <= char <= '\u9fff': # 中文字符 char_pinyin = pinyin(char, style=Style.NORMAL)[0][0] pinyin_list.append(char_pinyin) else: # 英文字符或数字 pinyin_list.append(char.lower()) return "".join(pinyin_list) def calculate_pinyin_similarity(text1, text2): """ 计算两个文本的拼音相似度 """ pinyin1 = text_to_pinyin(text1) pinyin2 = text_to_pinyin(text2) if not pinyin1 or not pinyin2: return 0 if pinyin2 in pinyin1 and len(text2)>=3: return 1.0 if pinyin1 in pinyin2 and len(text1)>=3: return 0.6 # 使用编辑距离计算相似度 return SequenceMatcher(None, pinyin1, pinyin2).ratio() def match_text(audio_text,detected_text): if not detected_text: return 0 similarity=0 audio_text = re.sub(r'[^\w\u4e00-\u9fff]', '', audio_text) detected_text = re.sub(r'[^\w\u4e00-\u9fff]', '', detected_text) if len(audio_text) == len(detected_text): similarity = calculate_pinyin_similarity(audio_text,detected_text) elif len(audio_text) >= len(detected_text): for i in range(len(audio_text)-len(detected_text)): similarity = max(similarity,calculate_pinyin_similarity(audio_text[i:i+len(detected_text)],detected_text)) else: for i in range(len(detected_text)-len(audio_text)): similarity = max(similarity,calculate_pinyin_similarity(detected_text[i:i+len(audio_text)],audio_text)) return similarity def find_best_ocr_match(audio_text, ocr_detections,frame_name, min_confidence=75): """ 在OCR结果中寻找与音频文字拼音最匹配的文本 """ if not audio_text or not ocr_detections: return None, 0 img = cv2.imread(frame_name) height, width = img.shape[:2] best_match = None best_similarity = -1 second_match = None second_similarity = -1 for detection in ocr_detections: if detection.get('Confidence', 0) < min_confidence: continue polygon = [[int(coord['X']), int(coord['Y'])] for coord in detection['Polygon']] min_x = min(p[0] for p in polygon) max_x = max(p[0] for p in polygon) min_y = min(p[1] for p in polygon) max_y = max(p[1] for p in polygon) box_center_y = (min_y + max_y) / 2 box_center_x = (min_x + max_x) / 2 detected_text = detection.get('DetectedText', '') detected_text = re.sub(r'[^\w\u4e00-\u9fff]', '', detected_text) if not detected_text : continue if len(detected_text) > len(audio_text)+3 and len(detected_text)<=5: continue # 计算拼音相似度 similarity = match_text(audio_text, detected_text) if len(detected_text)>=10: similarity = max(similarity,match_text(audio_text,detected_text[:len(detected_text)//2])) if len(audio_text)>=10: similarity = max(similarity,match_text(audio_text[:len(audio_text)//2],detected_text)) similarity = max(similarity,match_text(audio_text[len(audio_text)//2:],detected_text)) if len(audio_text)>=30: similarity = max(similarity,calculate_pinyin_similarity(audio_text[3*len(audio_text)//4:],detected_text)) similarity = max(similarity,calculate_pinyin_similarity(audio_text[:len(audio_text)//4],detected_text)) if len(ocr_detections)==1 and similarity<0.8: similarity+=0.2 if similarity<0.8 and (box_center_x>width*0.8 or box_center_xwidth*0.9 or box_center_x0.85: second_match = best_match second_similarity = best_similarity best_match = detection elif similarity > best_similarity: second_match = best_match second_similarity = best_similarity best_similarity = similarity best_match = detection else: if similarity == best_similarity: if len(best_match.get('DetectedText', '')) < len(detected_text): second_match = best_match second_similarity = best_similarity best_match = detection else: second_similarity = similarity second_match = detection else: if similarity == second_similarity: join1 =f"{second_match['DetectedText']} {detection['DetectedText']}" join2 =f"{detection['DetectedText']} {second_match['DetectedText']}" similarity1 = match_text(audio_text,join1) similarity2 = match_text(audio_text,join2) if similarity1>similarity2: second_match['DetectedTest'] = join1 else: second_match['DetectedTest'] = join2 if similarity>second_similarity: second_similarity = similarity second_match = detection if second_match: #print(second_match) polygon = [[int(coord['X']), int(coord['Y'])] for coord in best_match['Polygon']] min_x = min(p[0] for p in polygon) max_x = max(p[0] for p in polygon) min_y = min(p[1] for p in polygon) max_y = max(p[1] for p in polygon) # 计算文字框中心Y坐标 box_center_y = (min_y + max_y) / 2 box_center_x = (min_x + max_x) / 2 polygon = [[int(coord['X']), int(coord['Y'])] for coord in second_match['Polygon']] min_x2 = min(p[0] for p in polygon) max_x2 = max(p[0] for p in polygon) min_y2 = min(p[1] for p in polygon) max_y2 = max(p[1] for p in polygon) # 计算文字框中心Y坐标 box_center_y2 = (min_y2 + max_y2) / 2 box_center_x2 = (min_x2 + max_x2) / 2 if abs(box_center_y-box_center_y2)>2*(max_y-min_y) or min(min(abs(max_x2-min_x),abs(max_x-min_x2)),abs(box_center_x-box_center_x2))>4*(max_y-min_y): return best_match, best_similarity # 获取两个文本的Y坐标(使用Polygon的第一个点的Y值) best_y = best_match['Polygon'][0]['Y'] second_y = second_match['Polygon'][0]['Y'] # 按Y坐标从小到大排序 if max_x<=min_x2: combined_text = f"{best_match['DetectedText']} {second_match['DetectedText']}" elif max_x2<=min_x: combined_text = f"{second_match['DetectedText']} {best_match['DetectedText']}" elif best_y < second_y : combined_text = f"{best_match['DetectedText']} {second_match['DetectedText']}" else: combined_text = f"{second_match['DetectedText']} {best_match['DetectedText']}" similarity = match_text(audio_text,combined_text) #print(combined_text) #print(similarity) if similarity>=0.8 and len(combined_text)-len(audio_text)<5: best_match['DetectedText'] = combined_text if second_similarity >= 0.9 and similarity>=0.6: best_match['DetectedText'] = combined_text elif best_match is not None: if 'is_watermark' in best_match: if best_match['is_watermark'] and similarity<0.6: return None, -1 return best_match, best_similarity def calculate_english_ratio(text): """计算文本中英文字符的占比""" if not text: return 0 total_chars = len(text) if total_chars == 0: return 0 # 统计英文字符(字母、数字、英文标点) english_chars = 0 for char in text: # 判断是否为英文字符(包括数字和基本标点) if ('a' <= char <= 'z') or ('A' <= char <= 'Z') or ('0' <= char <= '9'): english_chars += 1 elif char in ' .,!?;:\'"-()[]{}': english_chars += 1 return english_chars / total_chars def _keep_main_cluster_per_line(boxes, grid_center_x, gap_char_mult=2.5, gap_min=80): """同一行内, 相邻框横向间隔过大(>gap_char_mult字宽 且 >gap_min像素)就断成多簇, 只保留【离字幕中心最近】的那一簇 → 把离得远的水印/角标从拼接里踢掉(不拼距离大的)。 多行字幕按行分别处理, 不影响正常换行。""" if len(boxes) <= 1: return boxes avg_h = sum(b['max_y'] - b['min_y'] for b in boxes) / len(boxes) lt = avg_h * 0.8 sb = sorted(boxes, key=lambda b: b['center_y']) lines = [[sb[0]]] for b in sb[1:]: lc = sum(x['center_y'] for x in lines[-1]) / len(lines[-1]) if abs(b['center_y'] - lc) <= lt: lines[-1].append(b) else: lines.append([b]) kept = [] for line in lines: line.sort(key=lambda b: b['center_x']) if len(line) == 1: kept.append(line[0]); continue ws = sorted((b['max_x'] - b['min_x']) / max(1, len(b['text'])) for b in line) cw = ws[len(ws) // 2] gap_thresh = max(cw * gap_char_mult, gap_min) clusters = [[line[0]]] for b in line[1:]: if b['min_x'] - clusters[-1][-1]['max_x'] > gap_thresh: clusters.append([b]) else: clusters[-1].append(b) if len(clusters) == 1: kept.extend(line) else: def cc(c): return (min(x['min_x'] for x in c) + max(x['max_x'] for x in c)) / 2 kept.extend(min(clusters, key=lambda c: abs(cc(c) - grid_center_x))) return kept def simple_smart_join(subtitle_boxes): """ 智能排序:按Y分行(行间从上到下),同行内按X从左到右。 分行判据用【垂直范围重叠率】而非"中心Y距离":上下两行几乎不重叠→必分行→上行在前; 同一行的左右两块Y范围高度重叠→同行→按X排。 (原来用"中心Y距离<0.8行高",字高抖动时会把上下两行误判同行→按X排→双行顺序翻车。) """ if not subtitle_boxes: return [] if len(subtitle_boxes) == 1: return subtitle_boxes # 按 center_y 排序后贪心分行:用【中心Y间隔】判同行(对OCR框高异常更稳健—— # 重叠率会被某个异常高的框骗到"同行"再按X排导致上下行翻转;中心Y间隔不受框高影响)。 # 中心Y间隔 < 0.5×较小框高 → 同行(左右两块,中心Y几乎相等);否则 → 上下两行。 sorted_boxes = sorted(subtitle_boxes, key=lambda b: b['center_y']) lines = [[sorted_boxes[0]]] for box in sorted_boxes[1:]: prev = lines[-1][-1] gap = abs(box['center_y'] - prev['center_y']) min_h = min(box['max_y'] - box['min_y'], prev['max_y'] - prev['min_y']) if min_h > 0 and gap < 0.5 * min_h: lines[-1].append(box) # 中心Y很近 → 同一行 else: lines.append([box]) # 中心Y拉开 → 新的一行(在下方) # 行间已是上→下(按center_y排过);同行内按 center_x 左→右 result = [] for line in lines: line.sort(key=lambda b: b['center_x']) result.extend(line) return result def extract_highest_subtitle(idx, all_pic_ocr_pair, best_grids, metadata_path,h,logger): """ 提取纵向跨度最大的字幕框(基于Y坐标比较) 根据帧的类型使用不同的最佳网格参数 参数: all_pic_ocr_pair: 图片OCR数据列表 [(pic_path1, ocr_json1), ...] best_grids: 包含三个组的最佳网格参数的字典 metadata_path: metadata.json文件路径 返回: 纵向跨度最大的字幕文本(或None) """ # 读取metadata.json获取帧的分类信息 with open(metadata_path, 'r', encoding='utf-8') as f: metadata = json.load(f) kept_first_names = set(metadata['kept_first']) kept_last_names = set(metadata['kept_last']) pic_path, ocr_json = all_pic_ocr_pair[idx] frame_name = os.path.basename(pic_path) # 确定帧的类型并选择相应的最佳网格参数 if frame_name in kept_first_names: grid_params = best_grids["kept_first"] h = h["kept_first"] group_name = "kept_first" hh=0.8*h elif frame_name in kept_last_names: grid_params = best_grids["kept_last"] h = h["kept_last"] group_name = "kept_last" hh=0.8*h else: grid_params = best_grids["composite"] h = h["composite"] group_name = "composite" hh=1.5*h if not grid_params: return "",90, True,0 best_grid, grid_width, grid_height, grid_top, grid_left = grid_params best_row, best_col = best_grid # 计算网格边界 grid_left = grid_left + best_col * grid_width grid_right = grid_left + grid_width grid_top = grid_top + best_row * grid_height grid_bottom = grid_top + grid_height # composite字幕条只包含字幕文字,X方向使用全图宽度作为容差 img_half_width = 50 * grid_width audio_text = metadata.get("audio_text", {}) # 如果是kept_first帧且有前三秒音频文字 if frame_name in kept_first_names and audio_text["first_3s"]!="!!" : if not audio_text.get("first_3s"): return "", 90, True,0 audio_str = audio_text["first_3s"] logger.log(f"前三秒音频文字: {audio_str}",program="getsrt") # 在OCR结果中寻找最佳匹配 best_detection, similarity = find_best_ocr_match(audio_str, ocr_json.get('TextDetections', []),pic_path) if best_detection and similarity > 0.65: # 相似度阈值可调整 matched_text = best_detection['DetectedText'] confidence = best_detection['Confidence'] logger.log(f"找到拼音匹配的OCR文本: {matched_text} (相似度: {similarity:.2f})",program="getsrt") return matched_text, similarity*50+confidence/2, True ,similarity logger.log("未找到足够相似的OCR匹配",program="getsrt") return "", 90, True, 0 # 音频文字置信度设为90 # 如果是kept_last帧且有后三秒音频文字 if frame_name in kept_last_names and audio_text["first_3s"]!="!!": if not audio_text.get("last_3s"): return "", 90, True, 0 audio_str = audio_text["last_3s"] logger.log(f"后三秒音频文字: {audio_str}",program="getsrt") # 在OCR结果中寻找最佳匹配 best_detection, similarity = find_best_ocr_match(audio_str, ocr_json.get('TextDetections', []),pic_path) #如果音频文字短,说明几乎没说话,对相似度要求更高 if len(audio_str)<5: if similarity<0.8: return "", 90, True, 0 if best_detection and similarity > 0.65: # 相似度阈值可调整 matched_text = best_detection['DetectedText'] confidence = best_detection['Confidence'] logger.log(f"找到拼音匹配的OCR文本: {matched_text} (相似度: {similarity:.2f})",program="getsrt") return matched_text, similarity*20+confidence*0.8, True,similarity logger.log("未找到足够相似的OCR匹配",program="getsrt") return "", 90, True,0 # 计算网格中心Y坐标 grid_center_y = (grid_top + grid_bottom) / 2 grid_center_x = (grid_left + grid_right) / 2 # 收集所有符合条件的字幕框 subtitle_boxes = [] if len(ocr_json['TextDetections'])==1: text_detection = ocr_json['TextDetections'][0] if text_detection['Confidence']<=60: return "", 90, True,0 polygon = [[int(coord['X']), int(coord['Y'])] for coord in text_detection['Polygon']] min_x = min(p[0] for p in polygon) max_x = max(p[0] for p in polygon) min_y = min(p[1] for p in polygon) max_y = max(p[1] for p in polygon) # 计算文字框中心Y坐标 box_center_y = (min_y + max_y) / 2 box_center_x = (min_x + max_x) / 2 if 'is_watermark' in text_detection: if text_detection['is_watermark']: if abs(box_center_y - grid_center_y) <= hh and not (max_x < grid_left or min_x > grid_right): return text_detection['DetectedText'],text_detection['Confidence'],False,0 return "", 90, True,0 if abs(box_center_y - grid_center_y) <= 3*hh and abs(box_center_x - grid_center_x) <= 3*hh: if text_detection['Confidence']<=80: return text_detection['DetectedText'],text_detection['Confidence'],False,0 return text_detection['DetectedText'],text_detection['Confidence'],True,0 for text_detection in ocr_json['TextDetections']: if text_detection['Confidence']<=60: continue if 'is_watermark' in text_detection: if text_detection['is_watermark']: continue polygon = [[int(coord['X']), int(coord['Y'])] for coord in text_detection['Polygon']] min_x = min(p[0] for p in polygon) max_x = max(p[0] for p in polygon) min_y = min(p[1] for p in polygon) max_y = max(p[1] for p in polygon) # 计算文字框中心Y坐标 box_center_y = (min_y + max_y) / 2 box_center_x = (min_x + max_x) / 2 if group_name in ["kept_first", "kept_last"]: hh = 0.5*h else: hh = 1.5*h hnow = max_y-min_y # 检查是否在Y轴方向接近最佳网格 if abs(box_center_y - grid_center_y) <= hh: # composite字幕条仅含字幕文字,使用全图宽度;其他组用2*hnow x_tol = img_half_width if group_name == "composite" else 2 * hnow if not (max_x < grid_left - x_tol or min_x > grid_right + x_tol): subtitle_boxes.append({ 'min_y': min_y, 'max_y': max_y, 'min_x': min_x, 'max_x': max_x, 'text': text_detection['DetectedText'], 'confidence': text_detection['Confidence'], 'center_x': box_center_x, 'center_y': box_center_y, # 添加中心Y坐标用于后续比较 'distance_to_center': abs(box_center_y - grid_center_y) # 添加距离中心的绝对值 }) else: if abs(box_center_x - grid_center_x) <= hh and not (max_y grid_bottom): subtitle_boxes.append({ 'min_y': min_y, 'max_y': max_y, 'min_x': min_x, 'max_x': max_x, 'text': text_detection['DetectedText'], 'confidence': text_detection['Confidence'], 'center_x': box_center_x, 'center_y': box_center_y, # 添加中心Y坐标用于后续比较 'distance_to_center': abs(box_center_y - grid_center_y) # 添加距离中心的绝对值 }) # 如果没有找到符合条件的字幕框,返回None if group_name in ["kept_first", "kept_last"] and not subtitle_boxes: # 尝试检测画面中央附近的字幕作为标题 return "", 90, True,0 subtitle_boxes = find_central_subtitles(ocr_json, grid_left, grid_right, grid_top, grid_bottom,pic_path) if (len(subtitle_boxes)==1): center_y = (subtitle_boxes[0]['min_y']+subtitle_boxes[0]['max_y'])/2 diff_y = (-subtitle_boxes[0]['min_y']+subtitle_boxes[0]['max_y']) length = (-subtitle_boxes[0]['min_x']+subtitle_boxes[0]['max_x'])/len(subtitle_boxes[0]['text']) for text_detection in ocr_json['TextDetections']: if text_detection['Confidence']<=20: continue if 'is_watermark' in text_detection: if text_detection['is_watermark']: continue if text_detection['DetectedText']==subtitle_boxes[0]['text']: continue polygon = [[int(coord['X']), int(coord['Y'])] for coord in text_detection['Polygon']] min_x = min(p[0] for p in polygon) max_x = max(p[0] for p in polygon) min_y = min(p[1] for p in polygon) max_y = max(p[1] for p in polygon) box_center_y = (min_y + max_y) / 2 box_center_x = (min_x + max_x) / 2 if abs(box_center_y - center_y)<=h/4 and abs(max_y-min_y-diff_y)<=h/2: if abs(min_x-subtitle_boxes[0]['max_x'])<2*length or abs(subtitle_boxes[0]['min_x']-max_x)<=2*length: subtitle_boxes.append({ 'min_y': min_y, 'max_y': max_y, 'min_x': min_x, 'max_x': max_x, 'text': text_detection['DetectedText'], 'confidence':text_detection['Confidence'], 'center_x': box_center_x, 'center_y': box_center_y, # 添加中心Y坐标用于后续比较 'distance_to_center': abs(box_center_y - grid_center_y) # 添加距离中心的绝对值 }) filtered_boxes = [] if len(subtitle_boxes) > 1: # 按距离网格中心的距离排序(从近到远) # 过滤掉英文占比超过80%的box filtered_boxes = [] for box in subtitle_boxes: text = box['text'] english_ratio = calculate_english_ratio(text) if (english_ratio <= 0.7 and box['confidence']>=75) : # 保留英文占比不超过80%的box filtered_boxes.append(box) subtitle_boxes = filtered_boxes if len(subtitle_boxes) > 1: # 先踢掉"离字幕中心太远"的框(水印/角标不与居中字幕拼接), 再排序拼接 if config.get_subtitle_config().get('split_far_boxes', True): subtitle_boxes = _keep_main_cluster_per_line(subtitle_boxes, grid_center_x) subtitle_boxes = simple_smart_join(subtitle_boxes) best_subtitle = "".join(box['text'] for box in subtitle_boxes) elif len(subtitle_boxes) == 1: #print(subtitle_boxes) best_subtitle = subtitle_boxes[0]['text'] if not subtitle_boxes: return "", 90, True,0 confidence = sum(box['confidence'] for box in subtitle_boxes) / len(subtitle_boxes) if best_subtitle: # 如果有找到字幕,返回文本和置信度 if (len(subtitle_boxes)>1 and confidence<=90 ): return best_subtitle, confidence,False,0 else: return best_subtitle, confidence,True,0 else: return "", 90, True,0 def _filter_watermark_by_position(all_pic_ocr_pair, logger, dom_frac=0.25, min_count=20, max_distinct=40, bx=80, by=30): """坐标+单文本占比 删水印框(在逐帧提取字幕【之前】跑)。 把所有帧的OCR框按【位置分桶】,统计每个位置的文字: 某【单一文本】占该位置 ≥dom_frac 且出现≥min_count次 → 一个文本霸占此位置 = 水印/台标/固定标语 → 整框剔除。 真字幕在同一位置是【很多不同句子轮流出现】(单文本占比极低,实测~1%),不会被删。 注意: 不能用"多样性(不同/总数)"判, 因为字幕每句显示3-5帧, 多样性天然~0.3 会误删整条主字幕带。""" def _norm(s): return re.sub(r"[\s\W_]+", "", s) def _key(det): poly = det.get("Polygon") or [] if len(poly) < 4: return None cx = sum(p["X"] for p in poly) / len(poly) cy = sum(p["Y"] for p in poly) / len(poly) return (int(cx // bx), int(cy // by)) bucket = defaultdict(list) for _pic, oj in all_pic_ocr_pair: for det in oj.get("TextDetections", []): k = _key(det) t = _norm(det.get("DetectedText", "")) if k and t: bucket[k].append(t) wm = set() for k, texts in bucket.items(): if len(texts) < min_count: continue cnt = Counter(texts) top = cnt.most_common(1)[0][1] distinct = len(cnt) # 水印: 单一文本占比高(>=dom_frac) 且 该位置不同文本少(= dom_frac and distinct < max_distinct: wm.add(k) if not wm: return 0 # 边缘列排除: 水印多固定在左/右下角, 字幕居中且与水印有间隔。若检测到的水印桶集中在某一边缘, # 把【该边缘整列(到字幕区之前)】都排除 → 连被OCR打碎、漂移的水印碎片一起清掉(字幕居中不受影响)。 all_xb = [k[0] for k in bucket] max_xb = max(all_xb) if all_xb else 0 edge_cols = set() if max_xb > 0: wm_xb = [k[0] for k in wm] left_wm = [x for x in wm_xb if x <= max_xb * 0.35] right_wm = [x for x in wm_xb if x >= max_xb * 0.65] if len(left_wm) >= max(2, len(wm_xb) * 0.5): # 水印主要在左边 edge_cols = {x for x in range(0, max(left_wm) + 1)} elif len(right_wm) >= max(2, len(wm_xb) * 0.5): # 水印主要在右边 edge_cols = {x for x in range(min(right_wm), max_xb + 1)} removed = 0 for _pic, oj in all_pic_ocr_pair: dets = oj.get("TextDetections", []) keep = [] for det in dets: k = _key(det) if k is not None and (k in wm or k[0] in edge_cols): removed += 1 else: keep.append(det) oj["TextDetections"] = keep if logger: logger.log(f"坐标水印过滤: {len(wm)}个固定位置判为水印(文字恒定), 删除{removed}个框", program="getsrt") return removed def _filter_static_text(results, logger, min_frac=0.25, min_prefix_len=6, min_count=10, strip_prefix=False): """静态文字行过滤: 把"很多帧反复出现的同一文字"(水印/台标/固定标题)从逐帧文本里剔除。 真字幕每条只在少数连续帧出现,占总帧比例很低(实测<5%);水印/台标占比很高(实测30%+)。 - 全文静态(水印/台标): 某文本归一化后在 >=min_frac 的有字幕帧出现 → 整帧抹空。【默认开,已验证安全】 - 静态前缀(固定标题): 某长前缀在 >=min_frac 的帧出现 → 剥掉该前缀。【默认关,strip_prefix=True 才启用: 有误删风险——若真字幕碰巧共享开头(如"小明:…")会被误剥】 results: list of (frame_idx, text)。返回过滤后的同结构 list。""" def _norm(s): return re.sub(r"[\s\W_]+", "", s) nonempty = [t.strip() for _, t in results if t and t.strip()] n = len(nonempty) if n < min_count: return results thr = max(min_count, int(min_frac * n)) static_full = {k for k, c in Counter(_norm(t) for t in nonempty).items() if k and c >= thr} static_pref = [] if strip_prefix: pref_cnt = Counter() for t in nonempty: for L in range(min_prefix_len, min(len(t), 40) + 1): pref_cnt[t[:L]] += 1 for p in sorted((p for p, c in pref_cnt.items() if c >= thr), key=len, reverse=True): if not any(q.startswith(p) for q in static_pref): static_pref.append(p) if not static_full and not static_pref: return results def _is_static_variant(key): # 与水印互为子串(截断/拼接变体,如 "红警魔鬼蓝" / "…bilibili…")也算水印 if key in static_full: return True for s in static_full: if len(key) >= 4 and (key in s or s in key): return True return False removed = stripped = 0 out = [] for i, t in results: if not t or not t.strip(): out.append((i, t)); continue ts = t.strip() if _is_static_variant(_norm(ts)): out.append((i, "")); removed += 1; continue for p in static_pref: if ts == p: ts = ""; removed += 1; break if ts.startswith(p) and len(ts) > len(p): ts = ts[len(p):].strip(); stripped += 1; break out.append((i, ts)) if logger and (removed or stripped): logger.log(f"静态文字过滤: 抹除{removed}帧(水印/台标), 剥固定前缀{stripped}帧; " f"静态项={list(static_full)[:2]}{static_pref[:2]}", program="getsrt") return out def _collapse_repeat(s): """折叠一行内"紧挨着重复"的内容(OCR/拼接把同一片读了两遍): 整句重复: '追了的话就把那人头打了追了的话就把那人头打了' → '追了...打了' 前缀重复: 'Ok我就解释这一次Ok我就解释这一次再有...' → 'Ok我就解释这一次再有...' 通用做法: 反复去掉【相邻重复块】(≥5字, 从长到短优先), 直到稳定。""" orig = (s or "").strip() t = orig if len(t) < 8: return s changed = True while changed: changed = False n = len(t) for L in range(n // 2, 4, -1): # 块长从长到短, 最短5字(避免误删短重复字) hit = -1 for i in range(0, n - 2 * L + 1): if t[i:i + L] == t[i + L:i + 2 * L]: hit = i; break if hit >= 0: t = t[:hit + L] + t[hit + 2 * L:] # 删掉重复的那一份 changed = True break # 近似二重: 切两半, 一半是另一半的前缀(第二遍OCR少读几字)→ 取较完整那半 h = len(t) // 2 a, b = t[:h], t[h:] if len(a) >= 5 and (a.startswith(b) or b.startswith(a)): t = a if len(a) >= len(b) else b return t if t != orig else s def generate_srt_from_results(results, output_path, fps=30, interval_seconds=1): """ 生成SRT字幕文件,合并相同的字幕文本 参数: results: 字幕文本列表 [(frame_idx, text), ...] output_path: 输出文件路径 fps: 视频帧率(用于精确时间计算,默认30帧/秒) interval_seconds: 字幕时间间隔(秒),默认0.6秒 """ if not results: print("警告: 没有字幕数据可生成") return # 合并相同的字幕文本 merged_subtitles = [] current_text = None start_time = 0 end_time = 0 for i, (frame_idx, text) in enumerate(results): if (interval_seconds == 0.33): cur_end = (i + 1) // 3 + (i + 1) % 3 * interval_seconds else: cur_end = (i + 1) * interval_seconds # 完全相同 → 延长 if text == current_text: end_time = cur_end # 新文本以当前文本为前缀(渐进蹦字)→ 更新为更长的文本,延长时间 # 注意:要求 current_text 非空。空串是任何字符串的前缀,若允许会把 # 新字幕误判为"空白段的延续",导致起始时间错误地继承前面空白的起点。 elif current_text and text and text.startswith(current_text): current_text = text end_time = cur_end # 当前文本以新文本为前缀(字幕开始收缩/切换)→ 正常保存,开始新段 else: if current_text is not None and i != 1 and end_time >= 1: merged_subtitles.append({ 'start': start_time, 'end': end_time, 'text': current_text }) current_text = text if (interval_seconds == 0.33): start_time = i // 3 + (i % 3) * interval_seconds else: start_time = i * interval_seconds end_time = cur_end # 添加最后一个字幕段 if current_text is not None and end_time-start_time>0.4: merged_subtitles.append({ 'start': start_time, 'end': end_time, 'text': current_text }) # 过滤空文本的字幕段 merged_subtitles = [sub for sub in merged_subtitles if sub['text'].strip()] # 后处理:合并相邻高度相似的字幕(空帧打断 / OCR标点不一致) def _norm_punct(t): """归一化标点(仅用于比对,不改输出): - ······ / ...... / … 统一为省略号占位符 - 横杠 - – — 与 '一'(yī) 统一:二者字形几乎相同,OCR 常互相误读""" t = re.sub(r'[·•]{2,}', '…', t) t = re.sub(r'\.{2,}', '…', t) t = re.sub(r'[-–—一]', '—', t) return t def _is_subseq(short, long): """short 是否为 long 的子序列(每个字符按序出现在 long 中)。 用于判定 OCR 漏字的同一句字幕(如漏掉中间的 '……这样')。""" it = iter(long) return all(ch in it for ch in short) def _containment(short, long): """较短串被较长串覆盖的比例(基于公共匹配块总长 / 较短串长度)。 对截断 + 分散错字的同一句鲁棒(如 '同砚'→'同观'、'糗事'→'摸事' 等零散OCR错)。""" if not short: return 0.0 sm = SequenceMatcher(None, short, long) matched = sum(blk.size for blk in sm.get_matching_blocks()) return matched / len(short) def _strip_all_punct(t): """去掉所有标点/符号/空白,只保留文字,用于核心内容比对。""" return re.sub(r'[\s·•.…,。!?、:;""‘’()()\-–—~!?,.:; ]+', '', t) def _common_prefix_len(x, y): n = 0 for i in range(min(len(x), len(y))): if x[i] == y[i]: n = i + 1 else: break return n def _similar(a, b): """两段文本高度相似:前缀关系 / 子序列关系 / 去标点后共享长前缀 / 相似度>=0.92 处理多种OCR差异:渐进蹦字、标点不一致(······vs......)、中间漏字、交叉缺失闪烁""" if not a or not b: return False if b.startswith(a) or a.startswith(b): return True # 后缀关系: 一方=另一方【去掉开头几个字】(OCR漏读开头, 如 一刀两刀→刀两刀) → 同一句。 # 限制: 短串≥3字, 只差≤3个开头字, 避免误合不同短句。 _s, _l = (a, b) if len(a) <= len(b) else (b, a) if len(_s) >= 3 and (len(_l) - len(_s)) <= 3 and _l.endswith(_s): return True # 标点归一化后再判断前缀关系(处理 ······ vs ...... 等OCR差异) an, bn = _norm_punct(a), _norm_punct(b) if bn.startswith(an) or an.startswith(bn): return True # 子序列关系:较短者是较长者的子序列 → OCR漏字的同一句(要求够长,避免短文本误判) short, long = (an, bn) if len(an) <= len(bn) else (bn, an) if len(short) >= 10 and _is_subseq(short, long): return True # 去掉所有标点后比对:处理"同一句的有损版、但损的部位不同"的OCR闪烁 ca, cb = _strip_all_punct(a), _strip_all_punct(b) if ca and cb: if ca == cb or ca.startswith(cb) or cb.startswith(ca): return True cs, cl = (ca, cb) if len(ca) <= len(cb) else (cb, ca) # 较短者是较长者子序列 if len(cs) >= 10 and _is_subseq(cs, cl): return True # 共享足够长的公共前缀(开头稳定、中/尾各自丢失不同部分的闪烁特征) # 绝对长度≥15字就是强信号(两条不同台词共享这么长的相同开头几乎不可能); # 较短时再要求占较短者较大比例,避免短文本误判。 cp = _common_prefix_len(ca, cb) if cp >= 15 or (cp >= 12 and cp >= 0.6 * len(cs)): return True # 覆盖率:较短串的绝大部分字按块出现在较长串中 → 同一句的截断/分散错字变体 if len(cs) >= 12 and _containment(cs, cl) >= 0.9: return True # 截断开头:较短串与较长串的等长前缀高度相似(允许个别OCR错字,如 砚/观、钧/钩) # → 短串是长串刚开始出现时的截断版。要求≥5字,保护"嗯""什么?"等短插话不被误合。 if 5 <= len(cs) < len(cl) and \ SequenceMatcher(None, cs, cl[:len(cs)]).ratio() >= 0.8: return True # 找公共前缀,去掉后各自剥离开头标点再判断前缀关系 prefix_len = _common_prefix_len(a, b) if prefix_len >= 4: # 剥离开头标点:含 ASCII句点. 中文标点 破折号 中点· 等 strip_re = r'^[-–—.…·•\s,。!?、:;]+' a_rest = re.sub(strip_re, '', a[prefix_len:]) b_rest = re.sub(strip_re, '', b[prefix_len:]) if b_rest.startswith(a_rest) or a_rest.startswith(b_rest): return True if abs(len(an) - len(bn)) > 5: return False return SequenceMatcher(None, an, bn).ratio() >= 0.92 def _dedup_pass(subs): out = [] for sub in subs: if out and _similar(out[-1]['text'], sub['text']): # 保留更长的文本,延伸结束时间 if len(sub['text']) >= len(out[-1]['text']): out[-1]['text'] = sub['text'] out[-1]['end'] = sub['end'] else: out.append(sub) return out # 反复去重直到稳定(定点迭代)。单遍不够:合并时"保留更长文本"会让 deduped[-1] # 中途变长,某些段只有在它变长后才与之相似,但那时已被追加——下一遍才能合并。 prev_len = -1 while len(merged_subtitles) != prev_len: prev_len = len(merged_subtitles) merged_subtitles = _dedup_pass(merged_subtitles) # 不再按"合并后字幕条数"卡最少数量——字幕少也放行, 只要真有内容(非空)就生成SRT。 if not merged_subtitles: return False # 生成SRT文件 with open(output_path, 'w', encoding='utf-8') as f: for i, subtitle in enumerate(merged_subtitles): # 格式化为SRT时间格式 (HH:MM:SS,mmm) def format_time(seconds): hours = int(seconds // 3600) minutes = int((seconds % 3600) // 60) seconds_remainder = seconds % 60 milliseconds = int((seconds_remainder - int(seconds_remainder)) * 1000) return f"{hours:02d}:{minutes:02d}:{int(seconds_remainder):02d},{milliseconds:03d}" srt_start = format_time(subtitle['start']) srt_end = format_time(subtitle['end']) # 写入SRT条目(折叠整句自我重复) f.write(f"{i+1}\n") f.write(f"{srt_start} --> {srt_end}\n") f.write(f"{_collapse_repeat(subtitle['text'])}\n\n") return True import string def check_subtitle_quality_enhanced(best_subtitle, strict_mode=True): """ 增强版字幕质量检测 """ if not best_subtitle or not best_subtitle.strip(): return True subtitle = best_subtitle.strip() total_chars = len(subtitle) if total_chars == 0: return True digit_count = 0 symbol_count = 0 for char in subtitle: if char.isdigit(): # 检测数字[2](@ref) digit_count += 1 elif char in string.punctuation: # 检测标点符号 symbol_count += 1 total_special_chars = digit_count + symbol_count ratio = total_special_chars / total_chars english_ratio = calculate_english_ratio(subtitle) # 严格模式:数字单独占比也不能过高 if strict_mode: digit_ratio = digit_count / total_chars if total_chars>=10 and digit_ratio<=0.9 and ratio<=0.9 and english_ratio<=0.6: return True # 数字占比超过30%或总特殊字符占比超过50%都视为不合格 flag = (digit_ratio <= 0.7) and (ratio <= 0.8) else: # 普通模式:只检查总特殊字符占比 flag = ratio <= 0.5 return flag def _has_chinese(text): """文本是否含中文字符。""" return any('一' <= ch <= '鿿' for ch in (text or "")) def processsrt(root_path,jsonl_path, srt_path, interval_seconds,logger): """主处理流程""" # 1. 加载数据 logger.start_program("getsrt") _sub_cfg = config.get_subtitle_config() require_chinese = _sub_cfg.get('require_chinese', True) # 只要中文字幕 redundant_ratio = float(_sub_cfg.get('redundant_merge_ratio', 0.85)) # 冗余阈值,越大越宽松 # metadata.json 和 composite 图片都在 composite 子目录中 composite_dir = os.path.join(root_path, "composite") local_json_path = os.path.join(composite_dir, "metadata.json") jsonl_path = os.path.join(composite_dir, jsonl_path) srt_path = os.path.join(composite_dir, srt_path) if not process_composite_ocr_to_jsonl(local_json_path, composite_dir, jsonl_path,logger): logger.log(f"视频{root_path}将被过滤,原因:空白复合图超过一半",level="warning",program="getsrt") with open(jsonl_path, "r", encoding='utf-8') as f: contents = f.readlines() return None,None,"空白复合图超过一半",contents,None,None,None with open(jsonl_path, "r", encoding='utf-8') as f: contents = f.readlines() srt_path = os.path.abspath(srt_path) # 转换为绝对路径 all_pic_ocr_pair = [] idx = 0 while idx < len(contents): try: if idx % 2 == 0 and idx + 1 < len(contents): pic_path = contents[idx].strip().split("pic_path: ")[-1] ocr_json = json.loads(contents[idx+1]) all_pic_ocr_pair.append([pic_path, ocr_json]) idx += 2 else: idx += 1 except (json.JSONDecodeError, KeyError, ValueError): idx += 1 # 1.4 低置信占比过滤: 整段OCR里低置信检测占比过高 → 非字幕内容(幻灯片/代码/满屏文字), 整段丢弃。 # 口播字幕低置信率仅2-8%, 幻灯片/编程视频高达50%+ (OCR硬读点线目录/数字/代码出垃圾)。 # 这类视频还吃3-5倍OCR、拖垮吞吐, 直接判非字幕跳过。min_det 保证样本足够才判, 短视频不误杀。 if _sub_cfg.get('filter_lowconf', True): _thr = int(_sub_cfg.get('lowconf_thr', 80)) _frac = float(_sub_cfg.get('lowconf_filter_frac', 0.35)) _min_det = int(_sub_cfg.get('lowconf_min_det', 200)) _confs = [d.get('Confidence', 0) for _p, _oj in all_pic_ocr_pair for d in _oj.get('TextDetections', [])] if len(_confs) >= _min_det: _lowr = sum(1 for c in _confs if c < _thr) / len(_confs) if _lowr > _frac: logger.log(f"视频{root_path}将被过滤,原因:非字幕内容(低置信占比{_lowr*100:.0f}%>" f"{_frac*100:.0f}%, 共{len(_confs)}检测)--多为幻灯片/代码/满屏文字", level="warning", program="getsrt") logger.end_program("getsrt") return None, None, "非字幕内容(低置信过多)", contents, None, None, None # 1.5 坐标水印过滤: 删掉"固定位置+文字恒定"的水印/台标框(在定位和提取之前) if _sub_cfg.get('filter_watermark_pos', True): _filter_watermark_by_position( all_pic_ocr_pair, logger, dom_frac=float(_sub_cfg.get('wm_dominant_frac', 0.5)), min_count=int(_sub_cfg.get('wm_min_count', 20))) # 2. 分析最佳字幕位置 best_grids = analyze_subtitle_blocks(all_pic_ocr_pair, local_json_path,logger) if not best_grids["kept_first"] and not best_grids["composite"] and not best_grids["kept_last"]: logger.log(f"视频{root_path}将被过滤,原因:未找到合适的字幕区域",level="warning",program="getsrt") return None,None,"未找到合适的字幕区域",contents,best_grids,None,None h = analyze_h(all_pic_ocr_pair, local_json_path) if h["composite"]==0: logger.log(f"视频{root_path}将被过滤,原因:冗余文字过多",level="warning",program="getsrt") return None,None,"冗余文字过多",contents,best_grids,None,h["composite"] # 3. 处理所有帧 results = [] recent_frames = [] tot_score = 0 tot_conf = 0 times = 0 merge_time = 0 last_text = "" last_flag = False blank=0 audio_frame = [] for idx in range(len(all_pic_ocr_pair)): if os.environ.get("LOG_VERBOSE") == "1": print(f"\r处理进度: {idx+1}/{len(all_pic_ocr_pair)}", end="") subtitle_text, confidence, flag,similarity = extract_highest_subtitle( idx, all_pic_ocr_pair, best_grids, local_json_path, h, logger ) if idx<9 or idx> len(all_pic_ocr_pair)-9: if similarity!=0: audio_frame.append(f"第{idx}帧找到拼音匹配的OCR文本: {subtitle_text} (相似度: {similarity:.2f})") else: audio_frame.append(f"第{idx}帧未找到拼音匹配的OCR文本") if (not check_subtitle_quality_enhanced(subtitle_text)) and confidence<=90: subtitle_text = "" # 只要中文字幕:纯英文/数字的(无中文字符)丢弃 if require_chinese and subtitle_text and not _has_chinese(subtitle_text): subtitle_text = "" current_result = { 'frame_idx': idx, 'text': subtitle_text if subtitle_text else "", 'confidence': confidence if confidence else 75, 'is_merged': False } if not flag: merge_time+=1 if (subtitle_text!=""): times+=1 tot_score=tot_score+ evaluate_text_quality_rules(subtitle_text) tot_conf=tot_conf + confidence if (idx == 0): if (subtitle_text==""): current_result['confidence']=0 recent_frames.append(current_result) results.append((current_result['frame_idx'],current_result['text'])) last_text = current_result['text'] last_confidence = current_result['confidence'] last_flag = flag best_frame = current_result continue if len(recent_frames) > 0: # 检查与最近帧的相似度 similarity = SequenceMatcher(None, subtitle_text, last_text).ratio() if (similarity < 0.75 and last_text not in subtitle_text and subtitle_text not in last_text) or last_text=="" or (abs(len(last_text)-len(subtitle_text))>2 and confidence>=90 and last_confidence>=90 and last_flag and flag): # 相似度阈值,可调整 if last_confidence - confidence>15 and confidence<70: current_result['text']="" if (subtitle_text!=""): last_text = subtitle_text last_confidence = current_result['confidence'] last_flag = flag blank=0 else: current_result['confidence']=0 results.append((current_result['frame_idx'],current_result['text'])) recent_frames.append(current_result) blank+=1 if blank >= 2: recent_frames = [] recent_frames.append(current_result) last_text = "" last_confidence = 90 continue if len(recent_frames) == 1: if current_result['confidence']<75 and recent_frames[0]['confidence']<85: idxx = recent_frames[0]['frame_idx'] results[idxx] = (idxx, "") recent_frames = [] recent_frames.append(current_result) if current_result['confidence']>=65: results.append((current_result['frame_idx'],current_result['text'])) else: results.append((current_result['frame_idx'],"")) else: if subtitle_text in last_text and subtitle_text!="" and subtitle_text!=last_text: current_result['text'] = last_text if last_text in subtitle_text and last_text!="" and current_result['confidence']>=90: current_result['is_merged']=True if (subtitle_text!=""): last_text = subtitle_text last_confidence = current_result['confidence'] last_flag = flag blank=0 else: current_result['confidence']=0 results.append((current_result['frame_idx'],current_result['text'])) recent_frames.append(current_result) blank+=1 if blank >= 2: recent_frames = [] recent_frames.append(current_result) last_text="" last_confidence = 90 continue recent_frames.append(current_result) if current_result['is_merged']: best_frame = current_result best_frame['confidence'] = max(recent_frames, key=lambda x: x['confidence'])['confidence'] else: best_frame = max(reversed(recent_frames), key=lambda x: x['confidence']) if best_frame['confidence']<65: best_frame['text']="" for frame in recent_frames: idxx = frame['frame_idx'] if idxx!=idx: results[idxx] = (idxx, best_frame['text']) last_text = best_frame['text'] last_confidence = best_frame['confidence'] current_result['text'] = best_frame['text'] results.append((current_result['frame_idx'],current_result['text'])) # 除零保护: 没有任何有效字幕帧(times==0)时直接判"字幕太少"(无字幕), # 否则 tot_score/times 会抛 ZeroDivisionError, 被上层包成 RuntimeError 记为 error # (线上这一类崩溃曾占全部 error 的 ~20%, 本质是无字幕, 应走 no_subtitle)。 if times == 0: logger.log(f"\n视频{root_path}将被过滤,原因:字幕太少(无有效字幕帧)", level="warning", program="getsrt") logger.end_program("getsrt") return None, None, "字幕太少", contents, best_grids, audio_frame, h["composite"] score = tot_score/times conf = tot_conf/times logger.log(f"{times}个有效字幕综合得分:{score}",program="getsrt") logger.log(f"{times}个有效字幕综合信任度:{conf/100}",program="getsrt") if (merge_time > redundant_ratio*len(all_pic_ocr_pair)): logger.log(f"\n视频{root_path}将被过滤,原因:冗余文字过多/水印占比过大",level="warning",program="getsrt") logger.end_program("getsrt") return None,None,"冗余文字太多",contents,best_grids,audio_frame,h["composite"] # 字幕帧占比(times/总帧数)不再单独卡阈值——字幕数少也放行, 是否真的"没内容"交给 # 后面 generate_srt_from_results() 的实际产出结果判断(它生成不出SRT才归"字幕太少")。 if (conf<80): logger.log(f"\n视频{root_path}将被过滤,原因:confidence太低",level="warning",program="getsrt") logger.end_program("getsrt") return None,conf,"可能是特殊字体,置信度太低",contents,best_grids,audio_frame,h["composite"] # 3.5 静态文字行过滤(剔除水印/台标/固定标题;过滤后若没剩字幕→走"字幕太少"判无字幕) if _sub_cfg.get('filter_static', True): results = _filter_static_text( results, logger, min_frac=float(_sub_cfg.get('static_frac', 0.25)), strip_prefix=_sub_cfg.get('strip_static_prefix', False)) # 4. 生成SRT文件 os.makedirs(os.path.dirname(srt_path), exist_ok=True) if not generate_srt_from_results(results, srt_path,interval_seconds=interval_seconds): logger.log(f"\n视频{root_path}将被过滤,原因:字幕太少",level="warning",program="getsrt") logger.end_program("getsrt") return None,None,"字幕太少",contents,best_grids,audio_frame,h["composite"] #generate_json_from_results(results, srt_path,interval_seconds=interval_seconds) logger.log(f"\n处理完成!字幕已保存到: {srt_path}",program="getsrt") logger.end_program("getsrt") return score,conf,srt_path,contents,best_grids,audio_frame,h["composite"] if __name__ == "__main__": processsrt("./split/6833783390639033604/composite","ocr_result.jsonl", "test.srt", 0.33)