#!/usr/bin/env python3 """ Subtitle Preview Contact Sheet · 字幕预览检查图生成器 ============================================== 每次烧字幕后自动抽帧出检查图。 功能: 1. 从视频中按字幕时间点抽帧 2. 生成检查图(contact sheet) 3. 可选:在检查图上标注字幕框、问题区域 4. 生成 HTML 预览页面 依赖: pip install opencv-python pillow numpy 用法: # 从视频抽帧生成检查图 python subtitle-preview-contact-sheet.py --video input.mp4 --subtitle-ass subtitles.ass --output preview.png # 生成 HTML 预览页面 python subtitle-preview-contact-sheet.py --video input.mp4 --subtitle-ass subtitles.ass --generate-html --output preview.html # 批量处理多集 python subtitle-preview-contact-sheet.py --batch ./videos/ --subtitle-dir ./ass/ --output-dir ./previews/ # 自定义抽帧参数 python subtitle-preview-contact-sheet.py --video input.mp4 --subtitle-ass subtitles.ass --max-frames 20 --cols 4 输出: - preview.png: 检查图(多帧拼接) - preview.html: HTML 预览页面(可选) - frames/: 抽出的关键帧(可选) 路径: video-ai-system/engines/subtitle-pipeline/preview-tools/subtitle-preview-contact-sheet.py """ import argparse import json import os import sys from pathlib import Path try: import cv2 import numpy as np from PIL import Image, ImageDraw, ImageFont except ImportError as e: print(f"[ERROR] 缺少依赖:{e}") print("请先安装:pip install opencv-python pillow numpy") sys.exit(1) def extract_frames_by_subtitle(video_path: str, ass_path: str, output_dir: str, max_frames: int = 20) -> list: """ 按字幕时间点从视频中抽帧 :param video_path: 视频文件路径 :param ass_path: ASS 字幕文件路径 :param output_dir: 输出目录 :param max_frames: 最大抽帧数量 :return: 抽出的帧文件路径列表 """ # 打开视频 cap = cv2.VideoCapture(video_path) if not cap.isOpened(): print(f"[ERROR] 无法打开视频:{video_path}") return [] fps = cap.get(cv2.CAP_PROP_FPS) total_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT)) print(f"[INFO] 视频信息:{total_frames} 帧,{fps} FPS") # 读取 ASS 字幕文件,获取字幕时间点 subtitle_times = [] try: with open(ass_path, "r", encoding="utf-8-sig") as f: lines = f.readlines() for line in lines: if line.startswith("Dialogue:"): # 解析 ASS 对话行 parts = line.split(",") if len(parts) >= 10: start_time = parts[1].strip() # 开始时间 end_time = parts[2].strip() # 结束时间 # 转换为秒 start_seconds = ass_time_to_seconds(start_time) end_seconds = ass_time_to_seconds(end_time) # 取字幕中间时间点作为抽帧时间 middle_time = (start_seconds + end_seconds) / 2 subtitle_times.append(middle_time) except Exception as e: print(f"[ERROR] 读取 ASS 文件失败:{e}") return [] if not subtitle_times: print("[WARN] 未找到字幕时间点,将按固定间隔抽帧") # 按固定间隔抽帧 interval = total_frames / max_frames subtitle_times = [i * interval / fps for i in range(max_frames)] print(f"[INFO] 找到 {len(subtitle_times)} 个字幕时间点") # 创建输出目录 frames_dir = os.path.join(output_dir, "frames") os.makedirs(frames_dir, exist_ok=True) # 抽帧 frame_paths = [] for i, timestamp in enumerate(subtitle_times[:max_frames]): # 跳转到指定时间 cap.set(cv2.CAP_PROP_POS_MSEC, timestamp * 1000) ret, frame = cap.read() if not ret: print(f"[WARN] 无法读取帧:{timestamp:.2f}s") continue # 保存帧 frame_path = os.path.join(frames_dir, f"frame_{i:04d}_{timestamp:.2f}s.png") cv2.imwrite(frame_path, frame) frame_paths.append(frame_path) print(f"[INFO] 已抽帧:{timestamp:.2f}s -> {frame_path}") cap.release() print(f"[OK] 共抽出 {len(frame_paths)} 帧") return frame_paths def ass_time_to_seconds(ass_time: str) -> float: """ 将 ASS 时间戳转换为秒 :param ass_time: ASS 时间戳(H:MM:SS.cc) :return: 秒数 """ parts = ass_time.split(":") hours = int(parts[0]) minutes = int(parts[1]) seconds_parts = parts[2].split(".") seconds = int(seconds_parts[0]) centiseconds = int(seconds_parts[1]) if len(seconds_parts) > 1 else 0 total_seconds = hours * 3600 + minutes * 60 + seconds + centiseconds / 100 return total_seconds def generate_contact_sheet(frame_paths: list, output_path: str, cols: int = 4, padding: int = 10): """ 生成检查图(多帧拼接) :param frame_paths: 帧文件路径列表 :param output_path: 输出图片路径 :param cols: 列数 :param padding: 内边距(像素) """ if not frame_paths: print("[ERROR] 没有帧文件") return False # 读取第一帧,获取尺寸 first_frame = Image.open(frame_paths[0]) frame_width, frame_height = first_frame.size # 计算拼接图尺寸 rows = (len(frame_paths) + cols - 1) // cols sheet_width = cols * (frame_width + padding) + padding sheet_height = rows * (frame_height + padding) + padding # 创建拼接图 sheet = Image.new("RGB", (sheet_width, sheet_height), (0, 0, 0)) # 拼接帧 for i, frame_path in enumerate(frame_paths): frame = Image.open(frame_path) # 计算位置 row = i // cols col = i % cols x = col * (frame_width + padding) + padding y = row * (frame_height + padding) + padding # 粘贴帧 sheet.paste(frame, (x, y)) # 添加时间戳标注 draw = ImageDraw.Draw(sheet) timestamp = os.path.basename(frame_path).split("_")[1].replace(".png", "") draw.text((x + 5, y + 5), f"{timestamp}", fill=(255, 255, 255)) # 保存 os.makedirs(os.path.dirname(os.path.abspath(output_path)), exist_ok=True) sheet.save(output_path) print(f"[OK] 检查图已生成:{output_path}") return True def generate_html_preview(frame_paths: list, output_path: str, video_path: str = None): """ 生成 HTML 预览页面 :param frame_paths: 帧文件路径列表 :param output_path: 输出 HTML 文件路径 :param video_path: 视频文件路径(可选) """ html_lines = [] html_lines.append("") html_lines.append("") html_lines.append("
") html_lines.append(" ") html_lines.append("视频:{os.path.basename(video_path)}
") html_lines.append(f"共 {len(frame_paths)} 帧
") html_lines.append("