Initial clean project import

This commit is contained in:
cat-shark
2026-06-19 18:41:41 +08:00
commit a13b804c7a
1306 changed files with 220568 additions and 0 deletions
+530
View File
@@ -0,0 +1,530 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
字幕封面生成器 - 一键生成视频字幕和封面
Subtitle & Cover Generator for AIGC Panel
"""
import os
import sys
import json
import argparse
from typing import Dict, List, Any, Tuple
from pathlib import Path
# 导入必要的库
try:
import cv2
import numpy as np
from PIL import Image, ImageDraw, ImageFont, ImageFilter, ImageEnhance, ImageOps
from rembg import remove
except ImportError as e:
print(f"Error: Missing required library - {e}", file=sys.stderr)
sys.exit(1)
class SubtitleCoverGenerator:
"""字幕封面生成器主类"""
def __init__(self, config: Dict[str, Any]):
"""
初始化生成器
Args:
config: 配置字典,包含视频路径、文案、样式等
"""
self.config = config
self.video_path = config.get('videoPath')
self.script_text = config.get('scriptText', '')
self.subtitle_style = config.get('subtitleStyle', 'default')
self.cover_style = config.get('coverStyle', 'default')
self.output_dir = config.get('outputDir', './output')
# 新增配置参数
self.is_portrait = config.get('isPortrait', False) # 是否为竖屏视频
self.cover_text = config.get('coverText', '') # 封面文字
self.cover_text_position = config.get('coverTextPosition', 'bottom') # 文字位置: top, center, bottom
self.cover_text_color = config.get('coverTextColor', '#FFFFFF') # 文字颜色
self.cover_text_size = config.get('coverTextSize', 50) # 文字大小
self.outline_color = config.get('outlineColor', '#FFD700') # 描边颜色
self.outline_width = config.get('outlineWidth', 3) # 描边宽度
self.blur_radius = config.get('blurRadius', 15) # 背景模糊度
self.zoom_level = config.get('zoomLevel', 1.0) # 封面放大倍数
# 确保输出目录存在
os.makedirs(self.output_dir, exist_ok=True)
def get_video_info(self) -> Dict[str, Any]:
"""
获取视频信息
Returns:
包含视频宽度、高度、时长等信息的字典
"""
try:
cap = cv2.VideoCapture(self.video_path)
if not cap.isOpened():
raise Exception("Could not open video file")
width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))
height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
fps = cap.get(cv2.CAP_PROP_FPS)
frame_count = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
duration = frame_count / fps if fps > 0 else 0
cap.release()
# 检测是否为竖屏视频
is_portrait = height > width
return {
'width': width,
'height': height,
'fps': fps,
'duration': duration,
'is_portrait': is_portrait
}
except Exception as e:
raise Exception(f"Error getting video info: {str(e)}")
def extract_frame(self, timestamp: float = 1.0) -> np.ndarray:
"""
从视频中提取关键帧
Args:
timestamp: 提取帧的时间戳(秒)
Returns:
提取的帧(numpy数组)
"""
try:
cap = cv2.VideoCapture(self.video_path)
# 设置到指定时间戳
fps = cap.get(cv2.CAP_PROP_FPS)
frame_number = int(timestamp * fps)
cap.set(cv2.CAP_PROP_POS_FRAMES, frame_number)
ret, frame = cap.read()
cap.release()
if not ret:
raise Exception("Failed to extract frame from video")
return frame
except Exception as e:
raise Exception(f"Error extracting frame: {str(e)}")
def remove_background(self, image: np.ndarray) -> Image.Image:
"""
使用rembg进行人物抠图
Args:
image: 输入图像(numpy数组)
Returns:
抠图后的PIL Image对象(带透明通道)
"""
try:
# 转换为PIL Image
pil_image = Image.fromarray(cv2.cvtColor(image, cv2.COLOR_BGR2RGB))
# 使用rembg移除背景
output = remove(pil_image)
return output
except Exception as e:
raise Exception(f"Error removing background: {str(e)}")
def generate_cover(self) -> str:
"""
生成视频封面
Returns:
生成的封面图片路径
"""
try:
# 获取视频信息
video_info = self.get_video_info()
self.is_portrait = video_info['is_portrait']
# 提取关键帧
frame = self.extract_frame(1.0)
if self.cover_style == 'default':
# 默认封面:直接使用提取的帧
cover_path = os.path.join(self.output_dir, 'cover.png')
cv2.imwrite(cover_path, frame)
elif self.cover_style == 'blur-bg':
# 背景虚化效果
cover_path = self._generate_blur_cover(frame)
elif self.cover_style == 'outline':
# 人物描边效果
cover_path = self._generate_outline_cover(frame)
elif self.cover_style == 'gradient':
# 渐变背景效果
cover_path = self._generate_gradient_cover(frame)
elif self.cover_style == 'split':
# 斜切设计效果
cover_path = self._generate_split_cover(frame)
else:
cover_path = os.path.join(self.output_dir, 'cover.png')
cv2.imwrite(cover_path, frame)
# 如果需要放大封面
if self.zoom_level != 1.0:
cover_path = self._zoom_cover(cover_path)
# 如果有文字需要添加
if self.cover_text.strip():
cover_path = self._add_text_to_cover(cover_path)
return cover_path
except Exception as e:
raise Exception(f"Error generating cover: {str(e)}")
def _zoom_cover(self, cover_path: str) -> str:
"""放大封面"""
try:
image = Image.open(cover_path)
# 计算新的尺寸
new_width = int(image.width * self.zoom_level)
new_height = int(image.height * self.zoom_level)
# 放大图像
zoomed = image.resize((new_width, new_height), Image.Resampling.LANCZOS)
# 如果是竖屏视频,需要裁剪中心区域
if self.is_portrait:
# 裁剪为原始尺寸的正方形区域
left = (zoomed.width - image.width) // 2
top = (zoomed.height - image.height) // 2
right = left + image.width
bottom = top + image.height
zoomed = zoomed.crop((left, top, right, bottom))
else:
# 横屏视频裁剪中心区域
left = (zoomed.width - image.width) // 2
top = (zoomed.height - image.height) // 2
right = left + image.width
bottom = top + image.height
zoomed = zoomed.crop((left, top, right, bottom))
# 保存放大后的封面
zoom_path = os.path.join(self.output_dir, 'cover_zoomed.png')
zoomed.save(zoom_path, quality=95)
return zoom_path
except Exception as e:
print(f"Warning: Could not zoom cover: {e}", file=sys.stderr)
return cover_path
def _add_text_to_cover(self, cover_path: str) -> str:
"""添加文字到封面"""
try:
image = Image.open(cover_path)
draw = ImageDraw.Draw(image)
# 尝试加载字体,如果失败则使用默认字体
try:
font = ImageFont.truetype("arial.ttf", self.cover_text_size)
except:
try:
font = ImageFont.truetype("DejaVuSans-Bold.ttf", self.cover_text_size)
except:
font = ImageFont.load_default()
# 计算文字位置
bbox = draw.textbbox((0, 0), self.cover_text, font=font)
text_width = bbox[2] - bbox[0]
text_height = bbox[3] - bbox[1]
if self.cover_text_position == 'top':
x = (image.width - text_width) // 2
y = 50
elif self.cover_text_position == 'center':
x = (image.width - text_width) // 2
y = (image.height - text_height) // 2
else: # bottom
x = (image.width - text_width) // 2
y = image.height - text_height - 50
# 添加描边效果
if self.outline_width > 0:
# 创建描边
outline_color = self._hex_to_rgb(self.outline_color)
for offset_x in range(-self.outline_width, self.outline_width + 1):
for offset_y in range(-self.outline_width, self.outline_width + 1):
if offset_x == 0 and offset_y == 0:
continue
draw.text((x + offset_x, y + offset_y), self.cover_text, fill=outline_color, font=font)
# 添加文字
text_color = self._hex_to_rgb(self.cover_text_color)
draw.text((x, y), self.cover_text, fill=text_color, font=font)
# 保存带文字的封面
text_path = os.path.join(self.output_dir, 'cover_with_text.png')
image.save(text_path, quality=95)
return text_path
except Exception as e:
print(f"Warning: Could not add text to cover: {e}", file=sys.stderr)
return cover_path
def _hex_to_rgb(self, hex_color: str) -> Tuple[int, int, int]:
"""将十六进制颜色转换为RGB元组"""
hex_color = hex_color.lstrip('#')
return tuple(int(hex_color[i:i+2], 16) for i in (0, 2, 4))
def _generate_blur_cover(self, frame: np.ndarray) -> str:
"""生成背景虚化封面"""
# 转换为PIL
image = Image.fromarray(cv2.cvtColor(frame, cv2.COLOR_BGR2RGB))
# 抠出人物
person = self.remove_background(frame)
# 背景虚化(使用配置的模糊度)
background = image.filter(ImageFilter.GaussianBlur(radius=self.blur_radius))
# 合成
background.paste(person, (0, 0), person)
# 保存
cover_path = os.path.join(self.output_dir, 'cover_blur.png')
background.save(cover_path, quality=95)
return cover_path
def _generate_outline_cover(self, frame: np.ndarray) -> str:
"""生成人物描边封面"""
# 转换为PIL
image = Image.fromarray(cv2.cvtColor(frame, cv2.COLOR_BGR2RGB))
# 抠出人物
person = self.remove_background(frame)
# 创建描边效果
# 将person转为numpy进行描边处理
person_np = np.array(person)
alpha = person_np[:, :, 3]
# 使用形态学操作创建描边
kernel = np.ones((self.outline_width * 2 + 1, self.outline_width * 2 + 1), np.uint8)
dilated = cv2.dilate(alpha, kernel, iterations=1)
outline = dilated - alpha
# 创建描边图层
outline_layer = Image.new('RGBA', person.size, (0, 0, 0, 0))
outline_draw = ImageDraw.Draw(outline_layer)
# 获取描边颜色
outline_color = self._hex_to_rgb(self.outline_color)
# 应用描边
for y in range(outline.shape[0]):
for x in range(outline.shape[1]):
if outline[y, x] > 0:
outline_layer.putpixel((x, y), outline_color + (255,))
# 合成
result = Image.new('RGBA', image.size)
result.paste(image, (0, 0))
result.paste(outline_layer, (0, 0), outline_layer)
result.paste(person, (0, 0), person)
# 保存
cover_path = os.path.join(self.output_dir, 'cover_outline.png')
result.save(cover_path, quality=95)
return cover_path
def _generate_gradient_cover(self, frame: np.ndarray) -> str:
"""生成渐变背景封面"""
# 转换为PIL
image = Image.fromarray(cv2.cvtColor(frame, cv2.COLOR_BGR2RGB))
# 抠出人物
person = self.remove_background(frame)
# 创建渐变背景
width, height = image.size
gradient = Image.new('RGB', (width, height), color=(0, 0, 0))
draw = ImageDraw.Draw(gradient)
# 绘制渐变
for y in range(height):
r = int(103 + (118 - 103) * y / height)
g = int(126 + (74 - 126) * y / height)
b = int(234 + (162 - 234) * y / height)
draw.line([(0, y), (width, y)], fill=(r, g, b))
# 合成
gradient.paste(person, (0, 0), person)
# 保存
cover_path = os.path.join(self.output_dir, 'cover_gradient.png')
gradient.save(cover_path, quality=95)
return cover_path
def _generate_split_cover(self, frame: np.ndarray) -> str:
"""生成斜切设计封面"""
# 转换为PIL
image = Image.fromarray(cv2.cvtColor(frame, cv2.COLOR_BGR2RGB))
width, height = image.size
# 创建斜切背景
background = Image.new('RGB', (width, height), color=(255, 255, 255))
draw = ImageDraw.Draw(background)
# 绘制斜切色块
draw.polygon([(0, 0), (width * 0.6, 0), (width * 0.4, height), (0, height)],
fill=(103, 126, 234))
draw.polygon([(width * 0.6, 0), (width, 0), (width, height), (width * 0.4, height)],
fill=(118, 74, 162))
# 应用半透明图片
image_enhanced = ImageEnhance.Brightness(image).enhance(0.8)
background.paste(image_enhanced, (0, 0))
# 保存
cover_path = os.path.join(self.output_dir, 'cover_split.png')
background.save(cover_path, quality=95)
return cover_path
def generate_subtitles(self) -> List[Dict[str, Any]]:
"""
生成字幕列表
Returns:
字幕列表,每个字幕包含文本、开始时间、结束时间
"""
try:
# 将文案按行分割
lines = [line.strip() for line in self.script_text.split('\n') if line.strip()]
# 生成字幕列表(简单示例,每句3秒)
subtitles = []
for i, line in enumerate(lines):
subtitle = {
'index': i + 1,
'text': line,
'start_time': i * 3.0,
'end_time': (i + 1) * 3.0,
'style': self.subtitle_style
}
subtitles.append(subtitle)
return subtitles
except Exception as e:
raise Exception(f"Error generating subtitles: {str(e)}")
def save_srt(self, subtitles: List[Dict[str, Any]]) -> str:
"""
保存SRT字幕文件
Args:
subtitles: 字幕列表
Returns:
SRT文件路径
"""
try:
srt_path = os.path.join(self.output_dir, 'subtitles.srt')
with open(srt_path, 'w', encoding='utf-8') as f:
for sub in subtitles:
# SRT格式
f.write(f"{sub['index']}\n")
f.write(f"{self._format_time(sub['start_time'])} --> {self._format_time(sub['end_time'])}\n")
f.write(f"{sub['text']}\n\n")
return srt_path
except Exception as e:
raise Exception(f"Error saving SRT file: {str(e)}")
def _format_time(self, seconds: float) -> str:
"""格式化时间为SRT格式"""
hours = int(seconds // 3600)
minutes = int((seconds % 3600) // 60)
secs = int(seconds % 60)
millis = int((seconds % 1) * 1000)
return f"{hours:02d}:{minutes:02d}:{secs:02d},{millis:03d}"
def generate_all(self) -> Dict[str, Any]:
"""
一键生成所有内容
Returns:
包含生成结果的字典
"""
try:
# 生成封面
print("Generating cover...", file=sys.stderr)
cover_path = self.generate_cover()
# 生成字幕
print("Generating subtitles...", file=sys.stderr)
subtitles = self.generate_subtitles()
srt_path = self.save_srt(subtitles)
result = {
'success': True,
'coverPath': cover_path,
'srtPath': srt_path,
'subtitles': subtitles,
'message': 'Generated successfully'
}
return result
except Exception as e:
return {
'success': False,
'error': str(e),
'message': f'Generation failed: {str(e)}'
}
def main():
"""主函数"""
parser = argparse.ArgumentParser(description='Subtitle & Cover Generator')
parser.add_argument('config_file', help='Configuration JSON file path')
args = parser.parse_args()
try:
# 读取配置文件
with open(args.config_file, 'r', encoding='utf-8') as f:
config = json.load(f)
# 创建生成器
generator = SubtitleCoverGenerator(config)
# 执行生成
result = generator.generate_all()
# 输出结果
print(json.dumps(result, ensure_ascii=False))
sys.exit(0 if result['success'] else 1)
except Exception as e:
error_result = {
'success': False,
'error': str(e),
'message': f'Error: {str(e)}'
}
print(json.dumps(error_result, ensure_ascii=False))
sys.exit(1)
if __name__ == '__main__':
main()