import json import shutil from pathlib import Path from typing import Optional, List, Dict, Any from loguru import logger from app.core.config import settings BGM_EXTENSIONS = {".wav", ".mp3", ".m4a", ".aac", ".flac", ".ogg", ".webm"} def _style_file_path(style_type: str) -> Path: return settings.ASSETS_DIR / "styles" / f"{style_type}.json" def _load_style_file(style_type: str) -> List[Dict[str, Any]]: style_path = _style_file_path(style_type) if not style_path.exists(): return [] try: with open(style_path, "r", encoding="utf-8") as f: data = json.load(f) if isinstance(data, list): return data except Exception as e: logger.error(f"Failed to load style file {style_path}: {e}") return [] def list_styles(style_type: str) -> List[Dict[str, Any]]: return _load_style_file(style_type) def get_style(style_type: str, style_id: Optional[str]) -> Optional[Dict[str, Any]]: if not style_id: return None for item in _load_style_file(style_type): if item.get("id") == style_id: return item return None def get_default_style(style_type: str) -> Optional[Dict[str, Any]]: styles = _load_style_file(style_type) if not styles: return None for item in styles: if item.get("is_default"): return item return styles[0] def list_bgm() -> List[Dict[str, Any]]: bgm_root = settings.ASSETS_DIR / "bgm" if not bgm_root.exists(): return [] items: List[Dict[str, Any]] = [] for path in bgm_root.rglob("*"): if not path.is_file(): continue if path.suffix.lower() not in BGM_EXTENSIONS: continue rel = path.relative_to(bgm_root).as_posix() items.append({ "id": rel, "name": path.stem, "ext": path.suffix.lower().lstrip(".") }) items.sort(key=lambda x: x.get("name", "")) return items def resolve_bgm_path(bgm_id: str) -> Optional[Path]: if not bgm_id: return None bgm_root = settings.ASSETS_DIR / "bgm" candidate = (bgm_root / bgm_id).resolve() try: candidate.relative_to(bgm_root.resolve()) except ValueError: return None if candidate.exists() and candidate.is_file(): return candidate return None def prepare_style_for_remotion( style: Optional[Dict[str, Any]], temp_dir: Path, prefix: str ) -> Optional[Dict[str, Any]]: if not style: return None prepared = dict(style) font_file = prepared.get("font_file") if not font_file: return prepared source_font = (settings.ASSETS_DIR / "fonts" / font_file).resolve() try: source_font.relative_to((settings.ASSETS_DIR / "fonts").resolve()) except ValueError: logger.warning(f"Font path outside assets: {font_file}") return prepared if not source_font.exists(): logger.warning(f"Font file missing: {source_font}") return prepared temp_dir.mkdir(parents=True, exist_ok=True) ext = source_font.suffix.lower() target_name = f"{prefix}{ext}" target_path = temp_dir / target_name try: shutil.copy(source_font, target_path) prepared["font_file"] = target_name if not prepared.get("font_family"): prepared["font_family"] = prefix except Exception as e: logger.warning(f"Failed to copy font {source_font} -> {target_path}: {e}") return prepared