Files
WYF-koubo/python/video_subtitle_cover_generator.py
T
2026-06-19 18:45:55 +08:00

943 lines
35 KiB
Python

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
字幕封面生成器 - 后端处理逻辑
"""
import os
import sys
import json
import time
import tempfile
import subprocess
from pathlib import Path
from typing import Dict, Any, Optional, List, Tuple
import logging
# ⚠️ 修复模块路径:确保 Python 能找到 modules 和其他本地模块
# 将脚本所在目录(python 目录)添加到 sys.path
script_dir = os.path.dirname(os.path.abspath(__file__))
if script_dir not in sys.path:
sys.path.insert(0, script_dir)
print(f"[PATH] Added to sys.path: {script_dir}", file=sys.stderr)
try:
import cv2
import numpy as np
from PIL import Image, ImageDraw, ImageFont, ImageFilter
import jieba
# import moviepy.editor # 暂时注释掉,需要时再启用
except ImportError as e:
print(f"缺少必要的依赖库: {e}")
print("请安装以下库:")
print("pip install opencv-python pillow jieba")
sys.exit(1)
try:
from modules.font_manager import get_font_manager
FONT_MANAGER_AVAILABLE = True
print("[PATH] font_manager loaded successfully", file=sys.stderr)
except ImportError as e:
FONT_MANAGER_AVAILABLE = False
print(f"[PATH] Warning: font_manager module not available: {e}", file=sys.stderr)
# 配置日志
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
)
class VideoSubtitleCoverGenerator:
"""字幕封面生成器"""
def __init__(self, config: Dict[str, Any]):
self.config = config
self.logger = logging.getLogger(__name__)
# 字幕样式预设
self.subtitle_presets = {
"classic": {
"fontFamily": "Arial",
"fontSize": 24,
"fontColor": "#FFFFFF",
"backgroundColor": "#000000",
"backgroundColorOpacity": 0.7,
"position": "bottom"
},
"modern": {
"fontFamily": "NotoSerifCJK-VF",
"fontSize": 28,
"fontColor": "#FFD700",
"backgroundColor": "#1a1a1a",
"backgroundColorOpacity": 0.8,
"position": "bottom"
},
"minimal": {
"fontFamily": "NotoSerifCJK-VF",
"fontSize": 22,
"fontColor": "#FFFFFF",
"backgroundColor": "",
"backgroundColorOpacity": 0,
"position": "center"
},
"bold": {
"fontFamily": "NotoSerifCJK-VF",
"fontSize": 30,
"fontColor": "#FF0000",
"backgroundColor": "#FFFF00",
"backgroundColorOpacity": 0.9,
"position": "top"
}
}
# 封面样式预设
self.cover_presets = {
"big-text": {
"timePointType": "random",
"customTimePoint": 10,
"textOverlay": "",
"fontSize": 72,
"fontColor": "white",
"position": "center",
"backgroundColor": True,
"backgroundOpacity": 60,
"maxWidth": 90,
"effectType": "blur",
"quality": 95
},
"search-card": {
"timePointType": "middle",
"customTimePoint": 50,
"textOverlay": "",
"fontSize": 48,
"fontColor": "white",
"position": "bottom-left",
"backgroundColor": True,
"backgroundOpacity": 80,
"maxWidth": 70,
"effectType": "none",
"quality": 95
},
"cut-angle": {
"timePointType": "start",
"customTimePoint": 5,
"textOverlay": "",
"fontSize": 56,
"fontColor": "yellow",
"position": "bottom-right",
"backgroundColor": False,
"backgroundOpacity": 0,
"maxWidth": 75,
"effectType": "gradient",
"quality": 95
},
"blur-person": {
"timePointType": "random",
"customTimePoint": 15,
"textOverlay": "",
"fontSize": 64,
"fontColor": "white",
"position": "bottom-center",
"backgroundColor": True,
"backgroundOpacity": 50,
"maxWidth": 85,
"effectType": "blur-person",
"quality": 95
}
}
def get_subtitle_preset(self, preset_id: str) -> Dict[str, Any]:
"""获取字幕预设样式"""
return self.subtitle_presets.get(preset_id, self.subtitle_presets["classic"])
def get_cover_preset(self, preset_id: str) -> Dict[str, Any]:
"""获取封面预设样式"""
return self.cover_presets.get(preset_id, self.cover_presets["big-text"])
def extract_frame_at_time(self, video_path: str, time_seconds: float) -> Optional[np.ndarray]:
"""从视频中提取指定时间的帧"""
try:
cap = cv2.VideoCapture(video_path)
if not cap.isOpened():
self.logger.error(f"无法打开视频文件: {video_path}")
return None
# 设置时间位置
cap.set(cv2.CAP_PROP_POS_MSEC, time_seconds * 1000)
ret, frame = cap.read()
cap.release()
if ret:
return frame
else:
self.logger.warning(f"无法读取视频帧: {time_seconds}s")
return None
except Exception as e:
self.logger.error(f"提取视频帧失败: {e}")
return None
def get_video_duration(self, video_path: str) -> float:
"""获取视频时长"""
try:
cap = cv2.VideoCapture(video_path)
if not cap.isOpened():
return 0
fps = cap.get(cv2.CAP_PROP_FPS)
frame_count = cap.get(cv2.CAP_PROP_FRAME_COUNT)
duration = frame_count / fps if fps > 0 else 0
cap.release()
return duration
except Exception as e:
self.logger.error(f"获取视频时长失败: {e}")
return 0
def get_optimal_frame_time(self, video_path: str, time_point_type: str, custom_time: float) -> float:
"""获取最佳帧时间点"""
duration = self.get_video_duration(video_path)
if time_point_type == "start":
return min(custom_time, duration * 0.1) # 前10%
elif time_point_type == "middle":
return duration * (custom_time / 100) # 百分比位置
elif time_point_type == "end":
return max(duration - custom_time, duration * 0.9) # 后10%
elif time_point_type == "random":
import random
# 随机选择前60%的位置,避免片尾
return random.uniform(0, duration * 0.6)
else:
return min(custom_time, duration * 0.5) # 默认中间位置
def apply_cover_effect(self, image: Image.Image, effect_type: str) -> Image.Image:
"""应用封面特效"""
if effect_type == "blur":
# 高斯模糊背景
return image.filter(ImageFilter.GaussianBlur(radius=5))
elif effect_type == "blur-person":
# 模拟人物虚化效果 (简单的中心区域保持清晰)
width, height = image.size
# 创建遮罩
mask = Image.new('L', (width, height), 128)
# 中心区域清晰
center_x, center_y = width // 2, height // 2
radius = min(width, height) // 4
for x in range(width):
for y in range(height):
dist = ((x - center_x) ** 2 + (y - center_y) ** 2) ** 0.5
if dist < radius:
mask.putpixel((x, y), 255) # 清晰
else:
mask.putpixel((x, y), 64) # 模糊
blurred = image.filter(ImageFilter.GaussianBlur(radius=3))
return Image.composite(image, blurred, mask)
elif effect_type == "gradient":
# 渐变效果
width, height = image.size
gradient = Image.new('L', (width, height), 0)
draw = ImageDraw.Draw(gradient)
# 创建对角线渐变
for x in range(width):
for y in range(height):
# 计算渐变值
factor = (x + y) / (width + height)
value = int(255 * (0.3 + 0.7 * factor)) # 30%-100% 不透明度
gradient.putpixel((x, y), value)
# 创建半透明图层
overlay = Image.new('RGBA', (width, height), (0, 0, 0, 0))
gradient_rgba = Image.new('RGBA', (width, height), (0, 0, 0, 128))
overlay = Image.composite(overlay, gradient_rgba, gradient)
return Image.alpha_composite(image.convert('RGBA'), overlay).convert('RGB')
else:
return image
def add_text_to_image(self, image: Image.Image, text: str, style_config: Dict[str, Any]) -> Image.Image:
"""在图片上添加文字"""
if not text.strip():
return image
img = image.copy()
draw = ImageDraw.Draw(img)
width, height = img.size
# 获取字体(支持自定义字体)
font_size = style_config.get('fontSize', 48)
font_name = style_config.get('fontName', 'NotoSerifCJK-VF') # 从配置获取字体名称
font_weight = style_config.get('fontWeight', 400)
font = self._load_font_safe(font_name, font_size, font_weight)
# 计算文字尺寸
bbox = draw.textbbox((0, 0), text, font=font)
text_width = bbox[2] - bbox[0]
text_height = bbox[3] - bbox[1]
# 限制最大宽度
max_width_percent = style_config.get('maxWidth', 80) / 100
max_width = int(width * max_width_percent)
if text_width > max_width:
# 如果文字太宽,需要调整字体大小
scale_factor = max_width / text_width
new_font_size = int(font_size * scale_factor)
try:
font = ImageFont.truetype(font_family, new_font_size)
except:
font = ImageFont.load_default()
# 重新计算文字尺寸
bbox = draw.textbbox((0, 0), text, font=font)
text_width = bbox[2] - bbox[0]
text_height = bbox[3] - bbox[1]
# 计算位置
position = style_config.get('position', 'center')
font_color = style_config.get('fontColor', 'white')
# 解析颜色
if isinstance(font_color, str) and font_color.startswith('#'):
r = int(font_color[1:3], 16)
g = int(font_color[3:5], 16)
b = int(font_color[5:7], 16)
font_color = (r, g, b)
elif font_color == 'white':
font_color = (255, 255, 255)
elif font_color == 'yellow':
font_color = (255, 255, 0)
else:
font_color = (255, 255, 255)
# 计算文字位置
if position == 'center':
x = (width - text_width) // 2
y = (height - text_height) // 2
elif position == 'bottom-left':
x = 50
y = height - text_height - 50
elif position == 'bottom-right':
x = width - text_width - 50
y = height - text_height - 50
elif position == 'bottom-center':
x = (width - text_width) // 2
y = height - text_height - 50
elif position == 'top-left':
x = 50
y = 50
elif position == 'top-right':
x = width - text_width - 50
y = 50
elif position == 'top-center':
x = (width - text_width) // 2
y = 50
else: # 默认居中
x = (width - text_width) // 2
y = (height - text_height) // 2
# 添加背景
background_color = style_config.get('backgroundColor', True)
background_opacity = style_config.get('backgroundOpacity', 70)
if background_color and background_opacity > 0:
# 创建半透明背景
padding = 20
bg_x1 = max(0, x - padding)
bg_y1 = max(0, y - padding)
bg_x2 = min(width, x + text_width + padding)
bg_y2 = min(height, y + text_height + padding)
# 创建半透明黑色背景
background = Image.new('RGBA', (width, height), (0, 0, 0, 0))
bg_draw = ImageDraw.Draw(background)
# 计算背景颜色 (黑色半透明)
alpha = int(255 * (background_opacity / 100))
bg_color = (0, 0, 0, alpha)
bg_draw.rectangle([bg_x1, bg_y1, bg_x2, bg_y2], fill=bg_color)
img = Image.alpha_composite(img.convert('RGBA'), background).convert('RGB')
# 重新创建draw对象
draw = ImageDraw.Draw(img)
# 绘制文字
draw.text((x, y), text, font=font, fill=font_color)
return img
def generate_cover_with_custom_template(self, video_path: str, output_path: str, template: Dict[str, Any]) -> bool:
"""使用自定义模板生成封面"""
try:
# 提取视频的中间帧
duration = self.get_video_duration(video_path)
time_point = duration * 0.5 # 使用中间帧
frame = self.extract_frame_at_time(video_path, time_point)
if frame is None:
self.logger.error("无法提取视频帧")
return False
# 转换为PIL图像
image = Image.fromarray(cv2.cvtColor(frame, cv2.COLOR_BGR2RGB))
width, height = image.size
# 1. 应用背景模糊(如果启用)
background_config = template.get('background', {})
if background_config.get('blurEnabled', False):
blur_radius = background_config.get('blurRadius', 15)
image = image.filter(ImageFilter.GaussianBlur(radius=blur_radius))
# 2. 处理人像描边(如果启用)
portrait_config = template.get('portrait', {})
if portrait_config.get('strokeEnabled', False):
# 创建绘图对象
draw = ImageDraw.Draw(image)
# 获取人像位置和大小(百分比转换为像素)
pos = portrait_config.get('position', {'x': 50, 'y': 50})
size = portrait_config.get('size', {'width': 50, 'height': 70})
portrait_x = int(pos['x'] / 100 * width)
portrait_y = int(pos['y'] / 100 * height)
portrait_width = int(size['width'] / 100 * width)
portrait_height = int(size['height'] / 100 * height)
# 绘制描边框
stroke_color = portrait_config.get('strokeColor', '#FFD700')
stroke_width = portrait_config.get('strokeWidth', 3)
stroke_type = portrait_config.get('strokeType', 'solid')
# 计算矩形边界
left = portrait_x - portrait_width // 2
top = portrait_y - portrait_height // 2
right = portrait_x + portrait_width // 2
bottom = portrait_y + portrait_height // 2
if stroke_type == 'dashed':
# 虚线描边
dash_pattern = portrait_config.get('strokeDashPattern', [10, 5])
# 简化实现:绘制虚线矩形
# 这里需要手动绘制虚线,PIL不直接支持
pass # 简化处理,暂时使用实线
# 绘制实线矩形
for i in range(stroke_width):
draw.rectangle(
[left - i, top - i, right + i, bottom + i],
outline=stroke_color,
width=1
)
# 3. 添加标题文字
titles_config = template.get('titles', {})
# 主标题
main_title = titles_config.get('main', {})
main_text = main_title.get('text', '')
if main_text:
self._draw_text_with_stroke(
image,
main_text,
main_title,
width,
height
)
# 副标题
sub_title = titles_config.get('sub', {})
sub_text = sub_title.get('text', '')
if sub_text:
self._draw_text_with_stroke(
image,
sub_text,
sub_title,
width,
height
)
# 4. 添加蒙版(如果有)
mask_config = template.get('mask', {})
mask_path = mask_config.get('imagePath', '')
if mask_path and os.path.exists(mask_path):
try:
mask_img = Image.open(mask_path).convert('RGBA')
# 获取蒙版位置和大小
mask_pos = mask_config.get('position', {'x': 10, 'y': 10})
mask_size = mask_config.get('size', {'width': 30, 'height': 20})
mask_opacity = mask_config.get('opacity', 0.8)
mask_width = int(mask_size['width'] / 100 * width)
mask_height = int(mask_size['height'] / 100 * height)
mask_x = int(mask_pos['x'] / 100 * width)
mask_y = int(mask_pos['y'] / 100 * height)
# 调整蒙版大小和透明度
mask_img = mask_img.resize((mask_width, mask_height), Image.Resampling.LANCZOS)
if mask_opacity < 1.0:
alpha = mask_img.split()[3]
alpha = alpha.point(lambda p: int(p * mask_opacity))
mask_img.putalpha(alpha)
# 将蒙版粘贴到图像上
image_rgba = image.convert('RGBA')
image_rgba.paste(mask_img, (mask_x - mask_width // 2, mask_y - mask_height // 2), mask_img)
image = image_rgba.convert('RGB')
except Exception as e:
self.logger.warning(f"添加蒙版失败: {e}")
# 保存图片
image.save(output_path, 'JPEG', quality=95)
self.logger.info(f"自定义模板封面生成成功: {output_path}")
return True
except Exception as e:
self.logger.error(f"生成自定义模板封面失败: {e}")
return False
def _load_font_safe(self, font_name: str = None, font_size: int = 24, font_weight: int = 400) -> ImageFont.FreeTypeFont:
"""
加载字体,支持自定义字体、ziti目录、系统字体
优先顺序:ziti目录 -> FontManager -> 系统字体 -> 默认字体
Args:
font_name: 字体名称(如 "微软雅黑""墨趣古风体" 等)
font_size: 字体大小
font_weight: 字体粗细 (100-1000)
Returns:
字体对象
"""
import platform
# 1. 首先直接尝试在 ziti 目录查找字体文件
if font_name:
# 多个可能的 ziti 目录路径(相对和绝对)
possible_ziti_paths = [
os.path.join(os.getcwd(), 'ziti'), # 当前工作目录
os.path.join(os.path.dirname(__file__), '..', 'ziti'), # 相对于脚本
os.path.join(os.path.dirname(__file__), '..', '..', 'ziti'), # 项目根目录
'ziti', # 相对路径
os.path.expanduser('~/ziti'), # 用户主目录
]
# 尝试找到 ziti 目录
for ziti_dir in possible_ziti_paths:
if os.path.isdir(ziti_dir):
self.logger.info(f"Found ziti directory: {ziti_dir}")
# 在 ziti 目录中查找字体文件
for ext in ['.ttf', '.otf', '.ttc']:
# 直接按字体名称查找
font_path = os.path.join(ziti_dir, font_name + ext)
if os.path.exists(font_path):
try:
return ImageFont.truetype(font_path, font_size)
except Exception as e:
self.logger.warning(f"Failed to load ziti font from {font_path}: {e}")
# 如果没有找到,扫描目录中所有文件进行模糊匹配
try:
for file in os.listdir(ziti_dir):
if not file.startswith('.') and (file.endswith('.ttf') or file.endswith('.otf') or file.endswith('.ttc')):
file_base = os.path.splitext(file)[0]
# 检查是否匹配(精确或包含)
if file_base == font_name or font_name in file_base:
font_path = os.path.join(ziti_dir, file)
try:
return ImageFont.truetype(font_path, font_size)
except Exception as e:
self.logger.warning(f"Failed to load matched font {font_path}: {e}")
except Exception as e:
self.logger.warning(f"Error scanning ziti directory: {e}")
# 2. 如果 ziti 目录查找失败,使用 FontManager
if font_name and FONT_MANAGER_AVAILABLE:
try:
font_manager = get_font_manager()
font_path = font_manager.find_font(font_name, font_weight)
if font_path and os.path.exists(font_path):
try:
return ImageFont.truetype(font_path, font_size)
except Exception as e:
self.logger.warning(f"Failed to load custom font from {font_path}: {e}")
except Exception as e:
self.logger.warning(f"Font manager error: {e}")
# 3. 降级:根据操作系统加载默认系统字体
system = platform.system()
font_paths = []
if system == 'Windows':
font_dir = None
font_paths = [
os.path.join(font_dir, 'NotoSerifCJK-VF'), # 微软雅黑
'', # 微软雅黑 Bold
'', # 黑体
'', # 宋体
]
elif system == 'Darwin': # macOS
font_paths = [
'/Library/Fonts/PingFang.ttc',
'/System/Library/Fonts/PingFang.ttc',
]
else: # Linux
font_paths = [
'/usr/share/fonts/truetype/wqy/wqy-zenhei.ttc',
]
# 尝试加载每个字体
for font_path in font_paths:
if os.path.exists(font_path):
try:
return ImageFont.truetype(font_path, font_size)
except:
continue
return ImageFont.load_default()
def _draw_text_with_stroke(self, image: Image.Image, text: str, config: Dict[str, Any], img_width: int, img_height: int):
"""在图片上绘制带描边的文字"""
if not text:
return
# 获取配置
font_size = config.get('fontSize', 60)
font_weight = config.get('fontWeight', 700)
font_name = config.get('fontName', 'NotoSerifCJK-VF') # 从配置获取字体名称
color = config.get('color', '#FFFFFF')
stroke_color = config.get('strokeColor', '#000000')
stroke_width = config.get('strokeWidth', 2)
position = config.get('position', {'x': 50, 'y': 20})
# 加载字体(使用改进的字体加载逻辑,支持自定义字体)
font = self._load_font_safe(font_name, font_size, font_weight)
# 计算文字位置
text_x = int(position['x'] / 100 * img_width)
text_y = int(position['y'] / 100 * img_height)
# 创建绘图对象
draw = ImageDraw.Draw(image)
# 绘制描边(多次绘制以模拟描边效果)
if stroke_width > 0:
for adj_x in range(-stroke_width, stroke_width + 1):
for adj_y in range(-stroke_width, stroke_width + 1):
if adj_x != 0 or adj_y != 0:
draw.text(
(text_x + adj_x, text_y + adj_y),
text,
font=font,
fill=stroke_color,
anchor='mm'
)
# 绘制主文字
draw.text(
(text_x, text_y),
text,
font=font,
fill=color,
anchor='mm'
)
def generate_cover(self, video_path: str, output_path: str, cover_config: Dict[str, Any]) -> bool:
"""生成封面图片"""
try:
# 获取最佳帧时间点
time_point = self.get_optimal_frame_time(
video_path,
cover_config.get('timePointType', 'random'),
cover_config.get('customTimePoint', 10)
)
# 提取帧
frame = self.extract_frame_at_time(video_path, time_point)
if frame is None:
self.logger.error("无法提取视频帧")
return False
# 转换为PIL图像
image = Image.fromarray(cv2.cvtColor(frame, cv2.COLOR_BGR2RGB))
# 应用特效
effect_type = cover_config.get('effectType', 'none')
if effect_type != 'none':
image = self.apply_cover_effect(image, effect_type)
# 添加文字
text_overlay = cover_config.get('textOverlay', '').strip()
if text_overlay:
image = self.add_text_to_image(image, text_overlay, cover_config)
# 保存图片
quality = cover_config.get('quality', 95)
image.save(output_path, 'JPEG', quality=quality)
self.logger.info(f"封面生成成功: {output_path}")
return True
except Exception as e:
self.logger.error(f"生成封面失败: {e}")
return False
def generate_subtitle_srt(self, text: str, video_duration: float) -> str:
"""生成字幕SRT文件内容"""
try:
# 使用jieba分词
words = list(jieba.cut(text.strip()))
# 计算字幕时长分布
total_chars = len(text)
if total_chars == 0:
return ""
# 平均每个字幕显示2-3秒
avg_duration = min(3.0, max(2.0, video_duration / max(1, total_chars / 10)))
srt_lines = []
current_time = 1.0 # 从1秒开始
for i, word in enumerate(words):
if not word.strip():
continue
start_time = current_time
end_time = min(current_time + avg_duration, video_duration - 0.5)
# 格式化时间 (HH:MM:SS,mmm)
def format_time(seconds):
hours = int(seconds // 3600)
minutes = int((seconds % 3600) // 60)
secs = int(seconds % 60)
ms = int((seconds % 1) * 1000)
return "02d"
start_str = format_time(start_time)
end_str = format_time(end_time)
srt_lines.append(f"{i+1}")
srt_lines.append(f"{start_str} --> {end_str}")
srt_lines.append(word)
srt_lines.append("") # 空行
current_time = end_time
return "\n".join(srt_lines)
except Exception as e:
self.logger.error(f"生成字幕SRT失败: {e}")
return ""
def add_subtitle_to_video(self, video_path: str, srt_path: str, output_path: str, subtitle_config: Dict[str, Any]) -> bool:
"""为视频添加字幕 - 简化的实现"""
try:
# 目前先复制原视频作为输出(字幕功能可以后续完善)
# 实际项目中可以使用 ffmpeg 或其他视频处理库
import shutil
shutil.copy2(video_path, output_path)
self.logger.info(f"视频复制成功 (字幕功能待完善): {output_path}")
return True
except Exception as e:
self.logger.error(f"复制视频失败: {e}")
return False
def parse_srt_time(self, time_str: str) -> float:
"""解析SRT时间格式"""
try:
# 格式: 00:00:01,000
parts = time_str.replace(',', ':').split(':')
if len(parts) >= 3:
hours = int(parts[0])
minutes = int(parts[1])
seconds = float(parts[2])
return hours * 3600 + minutes * 60 + seconds
except:
pass
return 0.0
def process(self, progress_callback=None) -> Dict[str, Any]:
"""主处理流程"""
try:
# 检查是否使用自定义模板
if self.config.get('customTemplate', False):
return self._process_with_custom_template(progress_callback)
model_config = self.config.get('modelConfig', {})
video_path = model_config.get('video', '')
if not video_path or not os.path.exists(video_path):
return {
'code': -1,
'msg': f'视频文件不存在: {video_path}'
}
# 创建输出目录
output_dir = os.path.dirname(video_path)
base_name = os.path.splitext(os.path.basename(video_path))[0]
# 更新进度: 初始化
if progress_callback:
progress_callback(0.1, "初始化处理...")
# 获取字幕和封面配置
subtitle_template = model_config.get('subtitleTemplate', 'classic')
cover_template = model_config.get('coverTemplate', 'big-text')
subtitle_style = self.get_subtitle_preset(subtitle_template)
cover_style = self.get_cover_preset(cover_template)
# 如果配置中有自定义样式,覆盖预设
if 'subtitleStyle' in model_config:
subtitle_style.update(model_config['subtitleStyle'])
if 'coverStyle' in model_config:
cover_style.update(model_config['coverStyle'])
# 更新进度: 生成字幕
if progress_callback:
progress_callback(0.2, "生成字幕文件...")
# 生成字幕SRT
video_duration = self.get_video_duration(video_path)
reference_text = model_config.get('referenceText', '')
srt_content = self.generate_subtitle_srt(reference_text, video_duration)
# 保存SRT文件
srt_path = os.path.join(output_dir, f"{base_name}_subtitles.srt")
with open(srt_path, 'w', encoding='utf-8') as f:
f.write(srt_content)
# 更新进度: 生成封面
if progress_callback:
progress_callback(0.5, "生成视频封面...")
# 生成封面
cover_path = os.path.join(output_dir, f"{base_name}_cover.jpg")
cover_success = self.generate_cover(video_path, cover_path, cover_style)
# 更新进度: 添加字幕到视频
if progress_callback:
progress_callback(0.7, "为视频添加字幕...")
# 生成带字幕的视频
video_with_subtitle_path = os.path.join(output_dir, f"{base_name}_with_subtitles.mp4")
subtitle_success = self.add_subtitle_to_video(video_path, srt_path, video_with_subtitle_path, subtitle_style)
# 更新进度: 完成
if progress_callback:
progress_callback(1.0, "处理完成")
result = {
'code': 0,
'msg': '处理成功',
'video': video_with_subtitle_path if subtitle_success else video_path,
'cover': cover_path if cover_success else None,
'srtPath': srt_path
}
return result
except Exception as e:
self.logger.error(f"处理失败: {e}")
return {
'code': -1,
'msg': f'处理失败: {str(e)}'
}
def _process_with_custom_template(self, progress_callback=None) -> Dict[str, Any]:
"""使用自定义模板处理"""
try:
video_path = self.config.get('videoPath', '')
template = self.config.get('template', {})
if not video_path or not os.path.exists(video_path):
return {
'code': -1,
'msg': f'视频文件不存在: {video_path}'
}
if not template:
return {
'code': -1,
'msg': '缺少自定义模板配置'
}
# 创建输出目录
output_dir = self.config.get('outputDir', os.path.dirname(video_path))
if not os.path.exists(output_dir):
os.makedirs(output_dir, exist_ok=True)
base_name = os.path.splitext(os.path.basename(video_path))[0]
# 更新进度: 初始化
if progress_callback:
progress_callback(0.1, "初始化自定义模板处理...")
# 更新进度: 生成封面
if progress_callback:
progress_callback(0.5, "使用自定义模板生成封面...")
# 生成封面
cover_path = os.path.join(output_dir, f"{base_name}_custom_cover.jpg")
cover_success = self.generate_cover_with_custom_template(video_path, cover_path, template)
# 更新进度: 完成
if progress_callback:
progress_callback(1.0, "处理完成")
result = {
'success': cover_success,
'coverPath': cover_path if cover_success else None,
'message': '自定义模板处理成功' if cover_success else '自定义模板处理失败'
}
return result
except Exception as e:
self.logger.error(f"自定义模板处理失败: {e}")
return {
'success': False,
'error': str(e),
'message': f'自定义模板处理失败: {str(e)}'
}
def main():
"""命令行入口"""
if len(sys.argv) < 2:
print("用法: python video_subtitle_cover_generator.py <config.json>")
sys.exit(1)
config_path = sys.argv[1]
try:
with open(config_path, 'r', encoding='utf-8') as f:
config = json.load(f)
generator = VideoSubtitleCoverGenerator(config)
result = generator.process()
# 输出JSON结果(单行,以便Node.js解析)
print(json.dumps(result, ensure_ascii=False))
except Exception as e:
print(json.dumps({
'success': False,
'error': str(e),
'message': f'执行失败: {str(e)}'
}, ensure_ascii=False))
if __name__ == "__main__":
main()