From 388da8c7062ad4fbb98b73e9fd1b74fc7677e3b7 Mon Sep 17 00:00:00 2001 From: xiaoxia Date: Mon, 27 Jul 2026 07:52:12 +0800 Subject: [PATCH] =?UTF-8?q?fix(code-quality):=20=E4=BF=AE=E5=A4=8Dsubtitle?= =?UTF-8?q?=5Frender=5Fengine.py=E4=B8=AD=E7=9A=84ruff=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../subtitle_render_engine.py | 263 +----------------- 1 file changed, 12 insertions(+), 251 deletions(-) diff --git a/apps/worker/video_processing/subtitle_render_engine.py b/apps/worker/video_processing/subtitle_render_engine.py index cb4b82e31..768f4317e 100755 --- a/apps/worker/video_processing/subtitle_render_engine.py +++ b/apps/worker/video_processing/subtitle_render_engine.py @@ -24,264 +24,25 @@ from __future__ import annotations import logging -from dataclasses import dataclass -from pathlib import Path from typing import Any +from pathlib import Path from video_processing.path_security import PathSecurityError, is_in_allowed_dirs, safe_resolve_path +from packages.domain.subtitle_style import ( + ALLOWED_SUBTITLE_EXTENSIONS, + DEFAULT_FONT_SIZE, + DEFAULT_POSITION, + SubtitleSegment, + SubtitleStyle, +) +from packages.domain.subtitle_style import escape_ass_text as _escape_ass_text # noqa: F401 向后兼容导出 +from packages.domain.subtitle_style import format_ass_time as _format_ass_time +from packages.domain.subtitle_style import wrap_text as _wrap_text + logger = logging.getLogger(__name__) -# ── 常量 ────────────────────────────────────────────────────────────────────── - -ALLOWED_SUBTITLE_EXTENSIONS = {".srt", ".ass", ".vtt", ".sub"} - -# 9宫格位置映射(ASS alignment 编号) -POSITION_ALIGNMENT = { - "top_left": 7, - "top_center": 8, - "top_right": 9, - "middle_left": 4, - "center": 5, - "middle_right": 6, - "bottom_left": 1, - "bottom_center": 2, - "bottom_right": 3, -} - -# 位置简称兼容 -POSITION_ALIASES = { - "top": "top_center", - "bottom": "bottom_center", - "middle": "center", - "left": "middle_left", - "right": "middle_right", -} - -DEFAULT_FONT = "思源黑体" -DEFAULT_FONT_SIZE = 24 -DEFAULT_COLOR = "#FFFFFF" -DEFAULT_STROKE_COLOR = "#000000" -DEFAULT_STROKE_WIDTH = 1.5 -DEFAULT_POSITION = "bottom_center" -DEFAULT_MAX_CHARS_PER_LINE = 20 - - -# ── 字幕样式配置 ──────────────────────────────────────────────────────────── - - -@dataclass -class SubtitleStyle: - """字幕样式配置.""" - - font_name: str = DEFAULT_FONT - font_size: int = DEFAULT_FONT_SIZE - font_color: str = DEFAULT_COLOR - bold: bool = False - italic: bool = False - - # 描边 - stroke_enabled: bool = True - stroke_color: str = DEFAULT_STROKE_COLOR - stroke_width: float = DEFAULT_STROKE_WIDTH - - # 阴影 - shadow_enabled: bool = False - shadow_color: str = "#000000" - shadow_offset_x: int = 2 - shadow_offset_y: int = 2 - shadow_blur: float = 0.0 - - # 背景框 - background_enabled: bool = False - background_color: str = "#000000" - background_opacity: float = 0.5 # 0.0 ~ 1.0 - background_padding: int = 8 - background_radius: int = 4 - - # 位置 - position: str = DEFAULT_POSITION # 9宫格位置名 - margin_v: int = 60 # 垂直边距 - margin_l: int = 40 # 左边距 - margin_r: int = 40 # 右边距 - - # 多行 - max_chars_per_line: int = DEFAULT_MAX_CHARS_PER_LINE - line_spacing: int = 0 # 行间距 - - # 动画 - fade_in: float = 0.0 # 淡入时长(秒) - fade_out: float = 0.0 # 淡出时长(秒) - animation_type: str = "none" # none/fade/slide/typewriter - - @classmethod - def from_dict(cls, config: dict[str, Any] | None) -> "SubtitleStyle": - """从字典创建样式配置,带安全类型转换.""" - if not config or not isinstance(config, dict): - return cls() - - def safe_str(key: str, default: str) -> str: - val = config.get(key, default) - return str(val) if val is not None else default - - def safe_int(key: str, default: int) -> int: - try: - return int(config.get(key, default)) - except (TypeError, ValueError): - return default - - def safe_float(key: str, default: float) -> float: - try: - return float(config.get(key, default)) - except (TypeError, ValueError): - return default - - def safe_bool(key: str, default: bool) -> bool: - return bool(config.get(key, default)) - - position = safe_str("position", DEFAULT_POSITION) - position = POSITION_ALIASES.get(position, position) - if position not in POSITION_ALIGNMENT: - position = DEFAULT_POSITION - - return cls( - font_name=safe_str("font", DEFAULT_FONT), - font_size=safe_int("size", DEFAULT_FONT_SIZE), - font_color=safe_str("color", DEFAULT_COLOR), - bold=safe_bool("bold", False), - italic=safe_bool("italic", False), - stroke_enabled=safe_bool("stroke_enabled", True), - stroke_color=safe_str("stroke_color", DEFAULT_STROKE_COLOR), - stroke_width=safe_float("stroke_width", DEFAULT_STROKE_WIDTH), - shadow_enabled=safe_bool("shadow_enabled", False), - shadow_color=safe_str("shadow_color", "#000000"), - shadow_offset_x=safe_int("shadow_offset_x", 2), - shadow_offset_y=safe_int("shadow_offset_y", 2), - shadow_blur=safe_float("shadow_blur", 0.0), - background_enabled=safe_bool("background_enabled", False), - background_color=safe_str("background_color", "#000000"), - background_opacity=max(0.0, min(1.0, safe_float("background_opacity", 0.5))), - background_padding=safe_int("background_padding", 8), - background_radius=safe_int("background_radius", 4), - position=position, - margin_v=safe_int("margin_v", 60), - margin_l=safe_int("margin_l", 40), - margin_r=safe_int("margin_r", 40), - max_chars_per_line=safe_int("max_chars_per_line", DEFAULT_MAX_CHARS_PER_LINE), - line_spacing=safe_int("line_spacing", 0), - fade_in=max(0.0, safe_float("fade_in", 0.0)), - fade_out=max(0.0, safe_float("fade_out", 0.0)), - animation_type=safe_str("animation_type", "none"), - ) - - @property - def alignment(self) -> int: - """获取 ASS alignment 编号.""" - return POSITION_ALIGNMENT.get(self.position, 2) - - @property - def ass_font_color(self) -> str: - """ASS 格式颜色 &HAABBGGRR.""" - return _hex_to_ass_color(self.font_color) - - @property - def ass_stroke_color(self) -> str: - return _hex_to_ass_color(self.stroke_color) - - @property - def ass_shadow_color(self) -> str: - return _hex_to_ass_color(self.shadow_color) - - @property - def ass_background_color(self) -> str: - """背景框颜色(ASS BackColour),带透明度.""" - alpha_hex = _opacity_to_ass_alpha(self.background_opacity) - color_bgr = _hex_to_ass_bgr(self.background_color) - return f"&H{alpha_hex}{color_bgr}" - - -# ── 工具函数 ────────────────────────────────────────────────────────────────── - - -def _hex_to_ass_color(hex_color: str) -> str: - """HEX → ASS 颜色 &HAABBGGRR(默认不透明).""" - hex_color = hex_color.lstrip("#") - if len(hex_color) != 6: - return "&H00FFFFFF" - r, g, b = hex_color[0:2], hex_color[2:4], hex_color[4:6] - return f"&H00{b.upper()}{g.upper()}{r.upper()}" - - -def _hex_to_ass_bgr(hex_color: str) -> str: - """HEX → ASS BGR 部分(不含 alpha).""" - hex_color = hex_color.lstrip("#") - if len(hex_color) != 6: - return "FFFFFF" - r, g, b = hex_color[0:2], hex_color[2:4], hex_color[4:6] - return f"{b.upper()}{g.upper()}{r.upper()}" - - -def _opacity_to_ass_alpha(opacity: float) -> str: - """不透明度 → ASS alpha(00=不透明,FF=完全透明).""" - alpha = 255 - int(opacity * 255) - return f"{alpha:02X}" - - -def _escape_ass_text(text: str) -> str: - """转义 ASS 文本特殊字符.""" - text = text.replace("\r\n", "\\N").replace("\n", "\\N").replace("\r", "\\N") - text = text.replace("{", "(").replace("}", ")") - return text - - -def _format_ass_time(seconds: float) -> str: - """秒 → ASS 时间格式 H:MM:SS.cc.""" - hours = int(seconds // 3600) - minutes = int((seconds % 3600) // 60) - secs = seconds % 60 - return f"{hours}:{minutes:02d}:{secs:05.2f}" - - -def _wrap_text(text: str, max_chars: int) -> list[str]: - """按字数换行,优先标点断开.""" - if len(text) <= max_chars: - return [text] - - lines: list[str] = [] - remaining = text - - while len(remaining) > max_chars: - break_point = max_chars - punctuations = ",。!?、;:,.;:!?" - - for i in range(max_chars, max_chars // 2, -1): - if i < len(remaining) and remaining[i] in punctuations: - break_point = i + 1 - break - - lines.append(remaining[:break_point]) - remaining = remaining[break_point:] - - if remaining: - lines.append(remaining) - - return lines - - -# ── 字幕片段 ────────────────────────────────────────────────────────────────── - - -@dataclass -class SubtitleSegment: - """单个字幕片段.""" - - start: float # 开始时间(秒) - end: float # 结束时间(秒) - text: str # 字幕文本 - style_name: str = "Default" # 使用的样式名 - - # ── 字幕渲染引擎 ──────────────────────────────────────────────────────────────