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
+312
View File
@@ -0,0 +1,312 @@
"""
工具函数模块
提供字幕封面生成过程中需要的通用工具函数
"""
import os
import sys
from pathlib import Path
from typing import Tuple, Optional, List, Dict, Any
# 尝试导入loguru,如果不可用则使用print作为替代
try:
from loguru import logger
except ImportError:
# Fallback logger that uses print
class logger:
@staticmethod
def info(msg, *args, **kwargs):
print(f"[INFO] {msg}", file=sys.stderr)
@staticmethod
def warning(msg, *args, **kwargs):
print(f"[WARN] {msg}", file=sys.stderr)
@staticmethod
def error(msg, *args, **kwargs):
print(f"[ERROR] {msg}", file=sys.stderr)
@staticmethod
def debug(msg, *args, **kwargs):
print(f"[DEBUG] {msg}", file=sys.stderr)
# 尝试导入图像处理库
try:
import cv2
import numpy as np
CV2_AVAILABLE = True
except ImportError:
CV2_AVAILABLE = False
logger.warning("opencv-python不可用,图像处理功能受限")
try:
from PIL import Image
PIL_AVAILABLE = True
except ImportError:
PIL_AVAILABLE = False
logger.warning("PIL不可用,图像处理功能受限")
def ensure_dir(path: str) -> Path:
"""确保目录存在"""
path_obj = Path(path)
path_obj.mkdir(parents=True, exist_ok=True)
return path_obj
def get_video_info(video_path: str) -> Dict[str, Any]:
"""获取视频信息"""
if not CV2_AVAILABLE:
# 如果opencv不可用,返回基本信息
try:
import os
file_size = os.path.getsize(video_path)
return {
'fps': 30, # 默认值
'frame_count': 0,
'width': 1920, # 默认值
'height': 1080, # 默认值
'duration': 0, # 未知
'path': video_path,
'file_size': file_size,
'note': '视频信息不完整,opencv不可用'
}
except Exception as e:
logger.error(f"获取基本视频信息失败: {e}")
raise
try:
cap = cv2.VideoCapture(video_path)
if not cap.isOpened():
raise ValueError(f"无法打开视频文件: {video_path}")
# 获取视频属性
fps = cap.get(cv2.CAP_PROP_FPS)
frame_count = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))
height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
duration = frame_count / fps if fps > 0 else 0
cap.release()
return {
'fps': fps,
'frame_count': frame_count,
'width': width,
'height': height,
'duration': duration,
'path': video_path
}
except Exception as e:
logger.error(f"获取视频信息失败: {e}")
raise
def extract_frame_at_time(video_path: str, time_seconds: float):
"""在指定时间抽取视频帧"""
if not CV2_AVAILABLE:
logger.warning("opencv不可用,无法抽取视频帧")
# 返回一个占位符
return None
try:
cap = cv2.VideoCapture(video_path)
if not cap.isOpened():
raise ValueError(f"无法打开视频文件: {video_path}")
# 设置帧位置
fps = cap.get(cv2.CAP_PROP_FPS)
frame_number = int(time_seconds * fps)
cap.set(cv2.CAP_PROP_POS_FRAMES, frame_number)
ret, frame = cap.read()
cap.release()
if not ret:
raise ValueError(f"无法读取帧: {frame_number}")
return frame
except Exception as e:
logger.error(f"抽帧失败: {e}")
raise
def save_image(image, output_path: str, quality: int = 95) -> None:
"""保存图像
如果输出路径是PNG格式,会保留alpha通道(透明背景)
如果是JPEG格式,会转换为RGB格式
"""
try:
ensure_dir(os.path.dirname(output_path))
is_png = output_path.lower().endswith('.png')
if CV2_AVAILABLE and isinstance(image, np.ndarray):
# 使用OpenCV保存
if is_png and image.shape[2] == 4:
# PNG格式且有alpha通道,保存为BGRAOpenCV使用BGR格式)
# 确保图像是BGRA格式(B, G, R, A
# 如果输入是RGBAR, G, B, A),需要转换为BGRA
if image.dtype != np.uint8:
image = image.astype(np.uint8)
# OpenCV的imwrite会自动处理BGRA格式
success = cv2.imwrite(output_path, image, [cv2.IMWRITE_PNG_COMPRESSION, 3])
elif is_png:
# PNG格式但没有alpha通道,转换为RGB
if image.shape[2] == 3:
success = cv2.imwrite(output_path, image, [cv2.IMWRITE_PNG_COMPRESSION, 3])
else:
# 如果是单通道,转换为3通道
if len(image.shape) == 2:
image = cv2.cvtColor(image, cv2.COLOR_GRAY2BGR)
success = cv2.imwrite(output_path, image, [cv2.IMWRITE_PNG_COMPRESSION, 3])
else:
# JPEG格式,确保是3通道BGR
if image.shape[2] == 4:
# 有alpha通道,先合成到白色背景
bgr = image[:, :, :3]
alpha = image[:, :, 3:4] / 255.0
white_bg = np.ones_like(bgr) * 255
image = (bgr * alpha + white_bg * (1 - alpha)).astype(np.uint8)
elif len(image.shape) == 2:
image = cv2.cvtColor(image, cv2.COLOR_GRAY2BGR)
success = cv2.imwrite(output_path, image, [cv2.IMWRITE_JPEG_QUALITY, quality])
if not success:
raise ValueError(f"保存图像失败: {output_path}")
elif PIL_AVAILABLE and hasattr(image, 'save'):
# 使用PIL保存
if is_png:
image.save(output_path, 'PNG', compress_level=3)
else:
# JPEG格式,确保是RGB
if image.mode == 'RGBA':
# 合成到白色背景
rgb = Image.new('RGB', image.size, (255, 255, 255))
rgb.paste(image, mask=image.split()[3])
image = rgb
image.save(output_path, 'JPEG', quality=quality)
else:
raise ValueError("没有可用的图像保存方法")
logger.info(f"图像已保存: {output_path}")
except Exception as e:
logger.error(f"保存图像失败: {e}")
raise
def format_time(seconds: float) -> str:
"""格式化时间为 HH:MM:SS.mmm"""
hours = int(seconds // 3600)
minutes = int((seconds % 3600) // 60)
secs = int(seconds % 60)
milliseconds = int((seconds % 1) * 1000)
return "02d"
def create_temp_dir(prefix: str = "subtitle_cover_") -> str:
"""创建临时目录"""
import tempfile
temp_dir = tempfile.mkdtemp(prefix=prefix)
logger.info(f"创建临时目录: {temp_dir}")
return temp_dir
def cleanup_temp_dir(temp_dir: str) -> None:
"""清理临时目录"""
try:
import shutil
if os.path.exists(temp_dir):
shutil.rmtree(temp_dir)
logger.info(f"清理临时目录: {temp_dir}")
except Exception as e:
logger.warning(f"清理临时目录失败: {e}")
def validate_file_exists(file_path: str, file_type: str = "文件") -> None:
"""验证文件是否存在"""
if not os.path.exists(file_path):
raise FileNotFoundError(f"{file_type}不存在: {file_path}")
if not os.path.isfile(file_path):
raise ValueError(f"{file_type}不是文件: {file_path}")
def get_file_size_mb(file_path: str) -> float:
"""获取文件大小(MB)"""
size_bytes = os.path.getsize(file_path)
return size_bytes / (1024 * 1024)
def calculate_aspect_ratio(width: int, height: int) -> float:
"""计算宽高比"""
return width / height if height > 0 else 0
def resize_image(image: np.ndarray, target_width: int, target_height: int,
keep_aspect_ratio: bool = True) -> np.ndarray:
"""调整图像大小"""
if keep_aspect_ratio:
# 保持宽高比
h, w = image.shape[:2]
aspect_ratio = w / h
if target_width / target_height > aspect_ratio:
# 目标更宽,以高度为准
new_width = int(target_height * aspect_ratio)
new_height = target_height
else:
# 目标更高,以宽度为准
new_width = target_width
new_height = int(target_width / aspect_ratio)
resized = cv2.resize(image, (new_width, new_height))
else:
# 不保持宽高比,直接缩放
resized = cv2.resize(image, (target_width, target_height))
return resized
def blend_images(background: np.ndarray, foreground: np.ndarray,
position: Tuple[int, int] = (0, 0)) -> np.ndarray:
"""将前景图像合成到背景图像上"""
x, y = position
h, w = foreground.shape[:2]
# 确保位置不超出边界
bg_h, bg_w = background.shape[:2]
x = max(0, min(x, bg_w - w))
y = max(0, min(y, bg_h - h))
# 创建ROI
roi = background[y:y+h, x:x+w]
# 如果前景有alpha通道,进行透明合成
if foreground.shape[2] == 4:
# 分离颜色和alpha通道
foreground_rgb = foreground[:, :, :3]
alpha = foreground[:, :, 3] / 255.0
# 扩展alpha到3通道
alpha = np.stack([alpha] * 3, axis=2)
# 透明合成
blended = foreground_rgb * alpha + roi * (1 - alpha)
background[y:y+h, x:x+w] = blended.astype(np.uint8)
else:
# 直接覆盖
background[y:y+h, x:x+w] = foreground
return background
def apply_blur(image: np.ndarray, kernel_size: int = 15) -> np.ndarray:
"""应用模糊效果"""
return cv2.GaussianBlur(image, (kernel_size, kernel_size), 0)
def apply_gradient_overlay(image: np.ndarray, color: Tuple[int, int, int],
opacity: float = 0.5) -> np.ndarray:
"""应用渐变覆盖"""
overlay = np.full_like(image, color, dtype=np.uint8)
return cv2.addWeighted(image, 1 - opacity, overlay, opacity, 0)