import json import os import logging from abc import ABC, abstractmethod from typing import Dict, Any, Optional from opentelemetry.trace import Status, StatusCode from util.exceptions import ( TemplateError, TemplateNotFoundError, TemplateValidationError, ) from util import api, oss from config.settings import get_storage_config from telemetry import get_tracer logger = logging.getLogger(__name__) class TemplateService(ABC): """模板服务抽象接口""" @abstractmethod def get_template(self, template_id: str) -> Optional[Dict[str, Any]]: """ 获取模板信息 Args: template_id: 模板ID Returns: Dict[str, Any]: 模板信息,如果不存在则返回None """ pass @abstractmethod def load_local_templates(self): """加载本地模板""" pass @abstractmethod def download_template(self, template_id: str) -> bool: """ 下载模板 Args: template_id: 模板ID Returns: bool: 下载是否成功 """ pass @abstractmethod def validate_template(self, template_info: Dict[str, Any]) -> bool: """ 验证模板 Args: template_info: 模板信息 Returns: bool: 验证是否通过 """ pass class DefaultTemplateService(TemplateService): """默认模板服务实现""" def __init__(self): self.templates: Dict[str, Dict[str, Any]] = {} self.storage_config = get_storage_config() def get_template(self, template_id: str) -> Optional[Dict[str, Any]]: """获取模板信息""" if template_id not in self.templates: # 尝试下载模板 if not self.download_template(template_id): return None return self.templates.get(template_id) def load_local_templates(self): """加载本地模板""" template_dir = self.storage_config.template_dir if not os.path.exists(template_dir): logger.warning("Template directory does not exist: %s", template_dir) return for template_name in os.listdir(template_dir): if template_name.startswith("_") or template_name.startswith("."): continue target_path = os.path.join(template_dir, template_name) if os.path.isdir(target_path): try: self._load_template(template_name, target_path) except Exception as e: logger.error("Failed to load template %s: %s", template_name, e) def download_template(self, template_id: str) -> bool: """下载模板""" tracer = get_tracer(__name__) with tracer.start_as_current_span("download_template") as span: try: span.set_attribute("template.id", template_id) # 获取远程模板信息 template_info = api.get_template_info(template_id) if template_info is None: logger.warning("Failed to get template info: %s", template_id) return False local_path = template_info.get("local_path") if not local_path: local_path = os.path.join( self.storage_config.template_dir, str(template_id) ) template_info["local_path"] = local_path # 创建本地目录 if not os.path.isdir(local_path): os.makedirs(local_path) # 下载模板资源 overall_template = template_info.get("overall_template", {}) video_parts = template_info.get("video_parts", []) self._download_template_assets(overall_template, template_info) for video_part in video_parts: self._download_template_assets(video_part, template_info) # 保存模板定义文件 template_file = os.path.join(local_path, "template.json") with open(template_file, "w", encoding="utf-8") as f: json.dump(template_info, f, ensure_ascii=False, indent=2) # 加载到内存 self._load_template(template_id, local_path) span.set_status(Status(StatusCode.OK)) logger.info("Template downloaded successfully: %s", template_id) return True except Exception as e: span.set_status(Status(StatusCode.ERROR)) logger.error("Failed to download template %s: %s", template_id, e) return False def validate_template(self, template_info: Dict[str, Any]) -> bool: """验证模板""" try: local_path = template_info.get("local_path") if not local_path: raise TemplateValidationError("Template missing local_path") # 验证视频部分 for video_part in template_info.get("video_parts", []): self._validate_template_part(video_part, local_path) # 验证整体模板 overall_template = template_info.get("overall_template", {}) if overall_template: self._validate_template_part(overall_template, local_path) return True except TemplateValidationError: raise except Exception as e: raise TemplateValidationError(f"Template validation failed: {e}") def _load_template(self, template_name: str, local_path: str): """加载单个模板""" logger.info("Loading template: %s (%s)", template_name, local_path) template_def_file = os.path.join(local_path, "template.json") if not os.path.exists(template_def_file): raise TemplateNotFoundError( f"Template definition file not found: {template_def_file}" ) try: with open(template_def_file, "r", encoding="utf-8") as f: template_info = json.load(f) except json.JSONDecodeError as e: raise TemplateError(f"Invalid template JSON: {e}") template_info["local_path"] = local_path try: self.validate_template(template_info) self.templates[template_name] = template_info logger.info("Template loaded successfully: %s", template_name) except TemplateValidationError as e: logger.error( "Template validation failed for %s: %s. Attempting to re-download.", template_name, e, ) # 模板验证失败,尝试重新下载 if self.download_template(template_name): logger.info("Template re-downloaded successfully: %s", template_name) else: logger.error("Failed to re-download template: %s", template_name) raise def _download_template_assets( self, template_part: Dict[str, Any], template_info: Dict[str, Any] ): """下载模板资源""" local_path = template_info["local_path"] # 下载源文件 if "source" in template_part: source = template_part["source"] if isinstance(source, str) and source.startswith("http"): _, filename = os.path.split(source) new_file_path = os.path.join(local_path, filename) oss.download_from_oss(source, new_file_path) if filename.endswith(".mp4"): from util.ffmpeg import re_encode_and_annexb new_file_path = re_encode_and_annexb(new_file_path) template_part["source"] = os.path.relpath(new_file_path, local_path) # 下载覆盖层 if "overlays" in template_part: for i, overlay in enumerate(template_part["overlays"]): if isinstance(overlay, str) and overlay.startswith("http"): _, filename = os.path.split(overlay) oss.download_from_oss(overlay, os.path.join(local_path, filename)) template_part["overlays"][i] = filename # 下载LUT if "luts" in template_part: for i, lut in enumerate(template_part["luts"]): if isinstance(lut, str) and lut.startswith("http"): _, filename = os.path.split(lut) oss.download_from_oss(lut, os.path.join(local_path, filename)) template_part["luts"][i] = filename # 下载音频 if "audios" in template_part: for i, audio in enumerate(template_part["audios"]): if isinstance(audio, str) and audio.startswith("http"): _, filename = os.path.split(audio) oss.download_from_oss(audio, os.path.join(local_path, filename)) template_part["audios"][i] = filename def _validate_template_part(self, template_part: Dict[str, Any], base_dir: str): """验证模板部分""" # 验证源文件 source_file = template_part.get("source", "") if ( source_file and not source_file.startswith("http") and not source_file.startswith("PLACEHOLDER_") ): if not os.path.isabs(source_file): source_file = os.path.join(base_dir, source_file) if not os.path.exists(source_file): raise TemplateValidationError(f"Source file not found: {source_file}") # 验证音频文件 for audio in template_part.get("audios", []): if not os.path.isabs(audio): audio = os.path.join(base_dir, audio) if not os.path.exists(audio): raise TemplateValidationError(f"Audio file not found: {audio}") # 验证LUT文件 for lut in template_part.get("luts", []): if not os.path.isabs(lut): lut = os.path.join(base_dir, lut) if not os.path.exists(lut): raise TemplateValidationError(f"LUT file not found: {lut}") # 验证覆盖层文件 for overlay in template_part.get("overlays", []): if not os.path.isabs(overlay): overlay = os.path.join(base_dir, overlay) if not os.path.exists(overlay): raise TemplateValidationError(f"Overlay file not found: {overlay}")