#!/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 ") 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()