Files
meijiaka-zy/python-api/app/services/material_service.py
T
小鱼开发 285257905e feat: 视频生成页面改造、字幕冻结修复及多项前端优化
- 修复字幕切换模板后冻结的 bug:ASS.js 新实例在视频播放中创建时收不到
  play/playing 事件,RAF 循环不会启动。创建实例后手动触发 play 事件。
- VideoGeneration 页面 overhaul:卡片点击预览、左右箭头导航、换一个素材、
  动态按钮文案和占位提示。
- 修复私有音色素材预览播放 trialUrl 的问题,改为播放 sourceUrl。
- 放宽空镜素材匹配逻辑:优先满足时长,fallback 到最近时长并随机选择。
- 隐藏脚本生成页面的时长滑块。
- 修复登录页和侧边栏标题渐变 WebKit 兼容问题。
- 清理旧计划文档、测试文件和临时脚本。
- 更新 Makefile、prompts、materials.json 等配置。
2026-04-23 23:17:10 +08:00

101 lines
3.3 KiB
Python

"""
空镜素材服务
============
从本地 JSON 配置文件加载素材库,提供匹配逻辑。
duration 从文件名 `_{N}s_` 中自动解析。
"""
import json
import random
import re
from pathlib import Path
# 正则:从文件名中提取时长,如 plumbing_10s_a23f8fcb.mp4 → 10
_DURATION_RE = re.compile(r"_(\d+)s_")
# 配置缓存(启动时加载,运行时只读)
_keywords: dict[str, str] = {}
_materials: dict[str, list[dict]] = {}
def _get_config_path() -> Path:
"""获取配置文件绝对路径"""
# 配置文件位于项目根目录的 config/ 下
return Path(__file__).resolve().parent.parent.parent / "config" / "materials.json"
def _parse_duration(url: str) -> float:
"""从 URL 文件名中解析时长(秒)"""
filename = url.split("/")[-1]
match = _DURATION_RE.search(filename)
if not match:
raise ValueError(f"无法从文件名解析时长: {filename}")
return float(match.group(1))
def load_config() -> None:
"""加载素材配置到内存缓存"""
global _keywords, _materials
config_path = _get_config_path()
if not config_path.exists():
raise FileNotFoundError(f"素材配置文件不存在: {config_path}")
with open(config_path, "r", encoding="utf-8") as f:
data = json.load(f)
_keywords = data.get("keywords", {})
raw_materials = data.get("materials", {})
# 解析每个素材的 duration
_materials = {}
for slug, entries in raw_materials.items():
parsed = []
for entry in entries:
url = entry["url"]
duration = _parse_duration(url)
parsed.append({"url": url, "duration": duration})
_materials[slug] = parsed
def match_material(scene: str, required_duration: float, exclude_urls: list[str] | None = None) -> dict | None:
"""
根据场景描述和所需时长匹配空镜素材
Args:
scene: 分镜场景描述
required_duration: 所需时长(秒)
exclude_urls: 已使用的素材 URL 列表(避免重复)
Returns:
匹配到的素材 {url, duration},无匹配返回 None
"""
exclude_urls = exclude_urls or []
for keyword, slug in _keywords.items():
if keyword in scene:
all_materials = _materials.get(slug, [])
if not all_materials:
return None
# 1. 优先找时长完全满足的
all_candidates = [m for m in all_materials if m["duration"] >= required_duration]
candidates = [m for m in all_candidates if m["url"] not in exclude_urls]
if candidates:
return random.choice(candidates)
if all_candidates:
return random.choice(all_candidates)
# 2. 没有完全满足的,找时长最接近的(同差值随机选)
min_diff = min(abs(m["duration"] - required_duration) for m in all_materials)
closest_all = [m for m in all_materials if abs(m["duration"] - required_duration) == min_diff]
# 排除已使用的
unused = [m for m in closest_all if m["url"] not in exclude_urls]
if unused:
return random.choice(unused)
# 全部用完则允许重复,从 closest_all 中随机
return random.choice(closest_all)
return None