285257905e
- 修复字幕切换模板后冻结的 bug:ASS.js 新实例在视频播放中创建时收不到 play/playing 事件,RAF 循环不会启动。创建实例后手动触发 play 事件。 - VideoGeneration 页面 overhaul:卡片点击预览、左右箭头导航、换一个素材、 动态按钮文案和占位提示。 - 修复私有音色素材预览播放 trialUrl 的问题,改为播放 sourceUrl。 - 放宽空镜素材匹配逻辑:优先满足时长,fallback 到最近时长并随机选择。 - 隐藏脚本生成页面的时长滑块。 - 修复登录页和侧边栏标题渐变 WebKit 兼容问题。 - 清理旧计划文档、测试文件和临时脚本。 - 更新 Makefile、prompts、materials.json 等配置。
101 lines
3.3 KiB
Python
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
|