diff --git a/apps/worker/video_processing/render_adapter.py b/apps/worker/video_processing/render_adapter.py index 68ca88619..ccf3b5222 100755 --- a/apps/worker/video_processing/render_adapter.py +++ b/apps/worker/video_processing/render_adapter.py @@ -461,13 +461,27 @@ class RenderAdapter: from packages.domain.preset_bgm import get_preset_bgm preset = get_preset_bgm(preset_id) - if preset and preset.audio_url: + if preset is None: + logger.warning("[plan_id=%s] [BGM] 预设BGM不存在: preset_id=%s", plan_id, preset_id) + elif not preset.audio_url: + logger.warning( + "[plan_id=%s] [BGM] 预设BGM未部署音频文件: preset_id=%s name=%s(audio_url 为空,请运维上传音频后填入 preset_bgm.py)", + plan_id, + preset_id, + preset.name, + ) + else: from video_processing.url_security import ( ALLOWED_AUDIO_MIME_TYPES, safe_download_file, ) - logger.info("[plan_id=%s] [BGM] 从预设库下载: preset_id=%s", plan_id, preset_id) + logger.info( + "[plan_id=%s] [BGM] 从预设库下载: preset_id=%s url=%s", + plan_id, + preset_id, + preset.audio_url[:80], + ) safe_download_file( preset.audio_url, str(bgm_file), @@ -480,7 +494,14 @@ class RenderAdapter: except Exception as e: logger.warning("[plan_id=%s] [BGM] 预设库下载失败: %s", plan_id, e) - logger.warning("[plan_id=%s] [BGM] 所有来源都无法获取BGM,跳过", plan_id) + logger.warning( + "[plan_id=%s] [BGM] 所有来源都无法获取BGM(enabled=%s audio_url=%s asset_id=%s preset_id=%s),跳过", + plan_id, + bool(bgm_config.get("enabled")), + "set" if audio_url else "empty", + asset_id[:12] + "…" if len(asset_id) > 12 else asset_id or "empty", + preset_id or "empty", + ) return None @staticmethod diff --git a/docs/preset-bgm-upload.md b/docs/preset-bgm-upload.md new file mode 100644 index 000000000..1063be5f8 --- /dev/null +++ b/docs/preset-bgm-upload.md @@ -0,0 +1,30 @@ +# 预设 BGM 音频文件放置目录 + +将下列 10 首免费可商用 BGM 的 mp3/m4a 文件按 `{preset_id}.mp3` 命名放到本目录: + +| preset_id | 名称 | 风格 | 时长(s) | 标签 | +|-------------------|----------|----------|---------|----------------------------| +| bgm_upbeat_001 | 阳光清晨 | upbeat | 120 | 轻快 阳光 吉他 vlog | +| bgm_upbeat_002 | 活力节拍 | upbeat | 95 | 轻快 电子 活力 运动 | +| bgm_upbeat_003 | 夏日漫步 | upbeat | 110 | 轻快 夏日 ukulele 旅行 | +| bgm_relax_001 | 静谧时光 | relax | 180 | 治愈 钢琴 安静 冥想 | +| bgm_relax_002 | 雨后森林 | relax | 150 | 治愈 自然 放松 环境音 | +| bgm_relax_003 | 月光奏鸣曲 | relax | 200 | 治愈 古典 钢琴 优雅(公版) | +| bgm_tech_001 | 未来科技 | tech | 85 | 科技 电子 未来感 数码 | +| bgm_tech_002 | 数据脉冲 | tech | 100 | 科技 极简 数据 AI | +| bgm_commerce_001 | 心动时刻 | commerce | 75 | 电商 时尚 动感 带货 | +| bgm_commerce_002 | 品质生活 | commerce | 90 | 电商 高端 品牌 品质 | + +放好后执行(需要在有 OSS 凭证的机器上): +```bash +export OSS_ENDPOINT=oss-cn-hangzhou.aliyuncs.com +export OSS_ACCESS_KEY_ID=xxx +export OSS_ACCESS_KEY_SECRET=xxx +export OSS_BUCKET_NAME=xiaoxia-autocut +python scripts/upload_preset_bgm.py +``` + +脚本会: +1. 上传文件到 OSS `preset/bgm/.mp3`,设置公共读 ACL +2. 自动改写 `packages/domain/preset_bgm.py` 把对应 `audio_url=""` 回填成公网 URL +3. 提示 `git commit & push` diff --git a/scripts/upload_preset_bgm.py b/scripts/upload_preset_bgm.py new file mode 100755 index 000000000..59a746d99 --- /dev/null +++ b/scripts/upload_preset_bgm.py @@ -0,0 +1,154 @@ +#!/usr/bin/env python3 +"""一键上传预设 BGM 到 OSS 并回填 preset_bgm.py audio_url。 + +用法(在服务器或本地有 OSS 凭证的机器上执行): + 1. 把 mp3 文件放到 ./bgm_assets/ (created by you) 目录下,文件名按 {preset_id}.mp3 命名: + bgm_upbeat_001.mp3 阳光清晨 + bgm_upbeat_002.mp3 活力节拍 + bgm_upbeat_003.mp3 夏日漫步 + bgm_relax_001.mp3 静谧时光 + bgm_relax_002.mp3 雨后森林 + bgm_relax_003.mp3 月光奏鸣曲 + bgm_tech_001.mp3 未来科技 + bgm_tech_002.mp3 数据脉冲 + bgm_commerce_001.mp3 心动时刻 + bgm_commerce_002.mp3 品质生活 + 2. 确保环境变量已设置: + OSS_ENDPOINT, OSS_ACCESS_KEY_ID, OSS_ACCESS_KEY_SECRET, OSS_BUCKET_NAME + 3. 运行:python scripts/upload_preset_bgm.py + 4. 脚本会上传到 OSS 路径 preset/bgm/.mp3,并自动改写 + packages/domain/preset_bgm.py 填入 public URL。 + 5. git commit & push 即可。 + +支持可选参数: + --bgm-dir DIR 本地 mp3 目录(默认 ./bgm_assets) + --oss-prefix PFX OSS key 前缀(默认 preset/bgm/) + --dry-run 只打印要做的操作,不上传不改写 + --public 上传后设置公共读 ACL(默认开启,safe_download 走公网 URL) +""" + +from __future__ import annotations + +import argparse +import os +import sys +from pathlib import Path + +REQUIRED_ENV = ("OSS_ENDPOINT", "OSS_ACCESS_KEY_ID", "OSS_ACCESS_KEY_SECRET", "OSS_BUCKET_NAME") +PRESET_BGM_PY = Path(__file__).resolve().parents[1] / "packages" / "domain" / "preset_bgm.py" + + +def main() -> int: + ap = argparse.ArgumentParser() + ap.add_argument("--bgm-dir", default="./bgm_assets") + ap.add_argument("--oss-prefix", default="preset/bgm/") + ap.add_argument("--dry-run", action="store_true") + ap.add_argument("--public", action="store_true", default=True) + args = ap.parse_args() + + bgm_dir = Path(args.bgm_dir) + if not bgm_dir.exists(): + print(f"[ERR] 目录不存在: {bgm_dir}", file=sys.stderr) + return 2 + + # 加载 preset_bgm.py 中所有 ID(简单 AST 抽取) + sys.path.insert(0, str(PRESET_BGM_PY.parents[2])) + from packages.domain.preset_bgm import PRESET_BGM_LIBRARY # type: ignore + + id_to_preset = {p.id: p for p in PRESET_BGM_LIBRARY} + + # 枚举本地文件 + local_files: dict[str, Path] = {} + for ext in (".mp3", ".m4a", ".aac", ".wav", ".ogg"): + for f in bgm_dir.glob(f"*{ext}"): + pid = f.stem + local_files[pid] = f + + missing_local = [pid for pid in id_to_preset if pid not in local_files] + unknown_local = [pid for pid in local_files if pid not in id_to_preset] + + print(f"[INFO] 预设 BGM 总数: {len(PRESET_BGM_LIBRARY)}") + print(f"[INFO] 本地找到对应文件: {len(local_files) - len(unknown_local)}") + if missing_local: + print(f"[WARN] 缺少本地音频文件的预设({len(missing_local)}):") + for pid in missing_local: + print(f" {pid} {id_to_preset[pid].name}") + if unknown_local: + print(f"[WARN] 本地文件未匹配任何 preset_id({len(unknown_local)}): {unknown_local}") + + if args.dry_run: + for pid, f in local_files.items(): + if pid in id_to_preset: + url = f"https://{os.environ.get('OSS_BUCKET_NAME', '')}.{os.environ.get('OSS_ENDPOINT', '')}/{args.oss_prefix}{f.name}" + print(f"[DRY] would upload {f} → {args.oss_prefix}{f.name} → {url}") + return 0 + + # 凭证检查 + for k in REQUIRED_ENV: + if not os.environ.get(k): + print(f"[ERR] 缺少环境变量 {k}", file=sys.stderr) + return 3 + + try: + import oss2 # type: ignore + except ImportError: + print("[ERR] 需要 oss2: pip install oss2", file=sys.stderr) + return 4 + + endpoint = os.environ["OSS_ENDPOINT"] + bucket_name = os.environ["OSS_BUCKET_NAME"] + auth = oss2.Auth(os.environ["OSS_ACCESS_KEY_ID"], os.environ["OSS_ACCESS_KEY_SECRET"]) + bucket = oss2.Bucket(auth, f"https://{endpoint}", bucket_name) + public_base = f"https://{bucket_name}.{endpoint}" + + pid_to_url: dict[str, str] = {} + for pid, f in local_files.items(): + if pid not in id_to_preset: + continue + key = f"{args.oss_prefix.rstrip('/')}/{f.name}" + print(f"[UPLOAD] {f} → oss://{bucket_name}/{key}") + headers = {"Content-Type": "audio/mpeg" if f.suffix == ".mp3" else "audio/mp4"} + if args.public: + bucket.put_object_from_file(key, str(f), headers=headers) + bucket.put_object_acl(key, oss2.OBJECT_ACL_PUBLIC_READ) + else: + bucket.put_object_from_file(key, str(f), headers=headers) + url = f"{public_base}/{key}" + pid_to_url[pid] = url + print(f" → {url}") + + if not pid_to_url: + print("[WARN] 没有文件被上传,不修改 preset_bgm.py") + return 0 + + # 回填 preset_bgm.py(按 id 精确替换 audio_url="" 为 audio_url="") + text = PRESET_BGM_PY.read_text(encoding="utf-8") + orig = text + for pid, url in pid_to_url.items(): + # 匹配 PresetBGM( ... id="pid", ... audio_url="", ... ) + # 简单替换:找到 id="pid" 行开始的 PresetBGM 构造块,把块内的 audio_url="" 替换 + import re + + block_pat = re.compile( + r'(PresetBGM\([^)]*?id="' + re.escape(pid) + r'"[^)]*?audio_url=)"[^"]*"', + re.DOTALL, + ) + new_text, n = block_pat.subn(rf'\1"{url}"', text, count=1) + if n == 0: + # 兜底:可能 audio_url 后面直接是 ),没赋值?不会,dataclass 有默认值 + print(f"[WARN] 未在 PresetBGM 块中找到 id={pid} 的 audio_url 字段,跳过回填") + continue + text = new_text + if text != orig: + PRESET_BGM_PY.write_text(text, encoding="utf-8") + print(f"[OK] 已回填 {len(pid_to_url)} 条 audio_url 到 {PRESET_BGM_PY}") + print( + "[NEXT] git diff packages/domain/preset_bgm.py && git add -A && git commit -m 'feat(domain): fill preset BGM audio_urls' && git push" + ) + else: + print("[WARN] preset_bgm.py 未发生变化(可能已填过)") + return 0 + + +if __name__ == "__main__": + sys.exit(main())