Files
ocr_data/tool/getsrt.py
2026-09-28 16:09:31 +08:00

2024 lines
90 KiB
Python
Raw Permalink Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 修改后的主处理流程
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_x<width*0.2):
continue
elif box_center_y<height*0.1:
continue
if 'is_watermark' in detection:
if detection['is_watermark'] and similarity<0.85 and (box_center_x>width*0.9 or box_center_x<width*0.1):
continue
if detection['is_watermark'] and similarity<0.8 and box_center_y<height*0.3:
continue
if abs(best_similarity-similarity)<0.1 and len(best_match.get('DetectedText', '')) < len(detected_text) and similarity>0.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_top or min_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) 且 该位置不同文本少(<max_distinct);
# 字幕带即便有重复, "不同句子"也有几十上百种, distinct 大 → 不会被判水印。
if top / len(texts) >= 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)