手机/电脑素材 → 拼接成片
何时用
- 手机插在电脑上,要从手机直接把照片 / 实况 / 视频拉到 Mac(不用 AirDrop、不用「图像捕捉」、不用手动导入)
- 手上有几段视频/实况/照片(无论来自手机还是电脑本地文件夹),要合成一条成片:
- 保真拼接 →
concat_clips.py - 有冲击力的短片(重排叙事、掐段、交叉淡化)→
impact_edit.py - 要卡点 → 先
beat_plan.py出片单,再impact_edit.py(见 D 段) - 要配乐 → 先问用户「音乐您自己加,还是我帮您加」(见「配乐三条路」的铁律);他说自己加就出无配乐母版 + 推荐剪映,让我加再走
pick_cue.py→mix_cue.py - 要片头封面页(首帧别是黑的)→
make_cover.py出封面图 + 写进片单(见 E 段)
- 保真拼接 →
- 成片配好乐后又发现音乐没贯穿全片 / 听着单调只有一个调子 → 见「三条实测硬事实」,别去调 ffmpeg 音量
- 成片在微信 / Finder / 相册里缩略图是一块黑("黑不溜秋")→ 加片头封面页(见 E 段)
- 混在视频里的静态照片要按秒数停留(可加推镜)
不适用:从 Mac 本地已存在的录屏做证据截图 → 用 video-to-evidence-layout;人工逐帧挑画面 → 用 chat-recording-evidence(帧册)。
代跑口径
快是硬要求。 一次代跑默认 ≤2 分钟交付;本技能有几处天生耗时的环节,按实测基线先把预期讲清楚,别让用户干等:
| 环节 | 实测基线(M5 Pro) |
|---|---|
| ACE-Step 生成 48s 配乐(batch 3) | 约 5 分钟(DiT 走原生 MLX) |
| 16s 成片渲染(impact_edit.py) | 30–60 秒 |
| pick_cue.py 筛窗口 | < 10 秒 |
| make_cover.py --scan 扫候选帧 | 数秒 |
禁止三种自造开销:① 为"刷新"重跑与已有成片等价的整链 ② 通读脚本源码 ③ 逐帧读图复核。
确认依据以脚本打印的定量结果为准:pick_cue/mix_cue 的逐拍 dB 与末两拍、ffprobe 的帧数 + r_frame_rate、signalstats 的首帧 YAVG。
逐帧目检留给人——agent 只对告警处(死拍、末两拍偏低、YAVG < 110)抽帧看。
已有等价产物先比等价(帧数 + 首帧亮度),只补缺的那一件。
🔴 铁律零:出片前先问「发哪个平台」,横竖两版都留
画幅不是技术问题,是投放问题——别替用户决定。 动手剪之前先问一句「这条要发哪里」:
| 投放场景 | 画幅 | |---|---| | 视频号 / 抖音 / 快手 / 小红书 / 手机朋友圈 | 竖版(3:4 → 1080×1440,或 9:16 → 1080×1920) | | 电脑上看 / B站 / 公众号内嵌 / 投屏 / 存档 | 横版(4:3 → 1440×1080,或 16:9 → 1920×1080) | | 用户没想好 / 说不确定 | 两版都出,都保留 |
默认动作:同一份片单跑两遍,交一对文件,文件名必须带后缀:
成片名_横版.mp4 (1440×1080)
成片名_竖版.mp4 (1080×1440)
不带后缀,过两天就分不清哪个是哪个。与素材多数派画幅一致的那版零损失,另一版用模糊铺底补边——但两版都要给,让用户按投放平台自己挑。
2026-10-04 实例:同一份片单出了横版与竖版各一条,用户的评价是「两个版本都挺好,你都保留下来」。 两版的剪辑点、顺序、转场完全一致,只有画布不同 → 所以「再出一版」的成本极低(只多跑一次脚本),别图省事只交一版。
🔴 铁律一:朝向必须按「真实显示尺寸」判,不能用编码尺寸
这是本技能踩过最贵的一个坑(2026-10-04 实测)。
iPhone 竖着拍的实况 = 「横着存(1920×1440)+ 挂一个 −90° 显示矩阵」。此时:
| 读法 | 得到 | 后果 |
|---|---|---|
| ffprobe -show_entries stream=width,height | 1920×1440(编码尺寸) | ❌ 把竖屏全判成横屏 |
| 算入旋转矩阵(真实显示尺寸) | 1440×1920(竖屏) | ✅ 正确 |
那一批 8 段实况,按编码尺寸看着是「7 横 1 竖」,真实是 6 竖 2 横。后果:选错画幅 → 6 段画面只占画布 56% 宽、两侧糊成一片。
实现要点(脚本已内置 media_util.display_size()):
- 旋转信息只在
frame的 side_data 里(3x3 displaymatrix→rotation),stream 层的 side_data_list 通常没有——只查 stream 会拿到 0,照样误判。 - 判断口径:
abs(rotation) % 180 == 90→ 宽高互换。 - 别用
ffprobe -show_frames -show_entries frame=width,height取旋转后尺寸:它也不应用旋转,仍是编码尺寸。
🔴 铁律二:静态图片必须先用 sips 转码,不能直接喂 ffmpeg
本机 ffmpeg 8.1.2 没有 libheif:直接读 .HEIC 只会解出内嵌的 512×512 缩略图(实测原图 3024×4032,ffmpeg 只认 512×512,且比例失真)。
正解:sips -s format png <in> --out <out.png>(macOS 自带,全分辨率)。脚本已内置(prepare_still())。
附带一个反直觉点:sips 转出的 PNG 会带着 EXIF + 旋转矩阵,于是 ffmpeg 读它时会自动旋转(autorotate,默认开)。这是对的,别加
-noautorotate——实测加了这个开关,画面会直接躺倒 90°。同一个实况的 HEIC 与 MOV 转出来的朝向是一致的,可互为校验。
🔴 铁律三:别试 Homebrew
brew install gphoto2 / ifuse 在本机沙箱内必败:sandbox-exec: sandbox_apply: Operation not permitted——Homebrew 解包 bottle 时会再套一层 sandbox-exec,与 WorkBuddy 自身沙箱嵌套冲突,加 dangerouslyDisableSandbox 也一样。
正解=pymobiledevice3(纯 Python,走系统自带 usbmuxd,无需 root、无需读 /var/db/lockdown)。
其余踩过的坑
- 判断手机在不在:
ioreg -p IOUSB -w0 | grep -i iphone。⚠️system_profiler SPUSBDataType在沙箱内恒返回空,不可信。设备随插拔消失/重现,别一看到空就下结论。 - 实况 = 两个文件:
IMG_xxxx.HEIC(封面)+ 同名IMG_xxxx.MOV(含声音)。手机里照片按批次分目录,/DCIM/下编号最大的(如111APPLE)通常最新。 - macOS 没有
timeout命令,脚本里别用。 - zsh 不会对未加引号的变量做词分割。
AS="/a /b"; cmd $AS会把整串当一个参数(bash 才会分开)→ 报GET_FILE_INFO failed之类的怪错。多文件要么写数组arr=(...); cmd "$arr[@]",要么${=AS},要么逐个列出。 concat+-c copy出来的文件会带坏标签(time_base=1/15360/r_frame_rate=120/1),部分播放器(含微信)会误判帧率 → 收尾必须重编一遍(见下)。- 段长必须用
-frames:v N钉死,不能只靠-t(2026-10-04 实测)。-ssinput seek 时首帧 PTS 往往不为 0,-t 1.8会少输出 1 帧(30fps 下 1.8s 段只得 53 帧)。 正解:-frames:v int(round(dur*fps))钉死帧数,-t只给余量(dur+0.1)。 这 33ms 会顺着 concat 转移到之后所有剪辑点上——卡点成片对帧敏感,别省这一步。 - 硬切路径刻意不加
-r FPS。concat filter遇到非整数帧的段会补帧:实测 9 段凭空多出 6 帧(0.2s),把整条拍网格推偏。 对照实验:concat demuxer + copy= 483 帧(精确)|concat filter= 489 帧|concat filter去掉-r= 483 帧。 → 硬切路径(XFADE <= 0.001)不加-r;只有 xfade 路径(本身会改帧率)才加。 - 静图段(
still_segment)是同一个坑。-loop 1 ... -t hold也少 1 帧(hold=1.8@30fps 只得 53 帧)。正解:视频-frames:v N+ 音频-t N/fps,两边等长。 单测:输出正好 54 帧 / 1.800000s。 - 跨工具的判据口径必须统一,否则"筛选觉得好、自检却拦下"。
pick_cue.py与mix_cue.py判「结尾还有没有音乐」必须同一口径:都用「末两拍均值 vs 窗口内中位,容差 12dB」。 踩过的坑:mix_cue曾用「末两拍最小值 vs 全曲中位」+ 容差 8dB → 把正常的切分空拍(单拍掉 18–23dB 属正常重音) 误判成"音乐消失",拦下一首其实合格的曲子(末两拍 22/39:均值只低 8.3,最小值却低 15)。真正的病灶是整段静音 (末两拍 −4dB,相对中位低 42dB)→ 再补一条单拍硬线 25dB 兜底。 - 量切点别用
-ss t逐帧探测。-ss t输出的是 ≥t 的第一帧,不是"第 t 秒那一帧" → 会得出"封面切点偏早 33ms"这类假结论。 实测逐帧序列:封面段正好 54 帧 = 1.8000s,精确落在第 3 拍。 → 判切点用逐帧序列统计(select='eq(n,N)'),别用-ss。
内容级去重(必做,别只比文件名/体积/md5)
同一段实况被反复"存储"会产生内容相同、文件名与 md5 都不同的副本(实测:IMG_1984 / 1985 / 1992 画面逐帧一致,体积都恰好 5.21MB,md5 三个全不同)。体积与 md5 都不能用来判重。
可靠办法是跨时间抽帧做内容指纹:取 0.4/0.9/1.4/1.9/2.4/2.8s 六帧 → 各自缩到 64×64 → 求 md5 → 六个哈希串起来再求 md5。指纹相同即同一段,只保留第一个。
⚠️ 只比"中间那一帧"不够——不同瞬间可能撞上同一构图。
照片与实况封面别搞混:IMG_1990.HEIC 和 IMG_1990.MOV 是同一张画面的封面 + 动态。concat_clips.py 已自动跳过与视频同名的 HEIC(否则同一画面会先停 3 秒再播一遍)。要当独立照片用,改个名(如 PHOTO_0001.jpg)。
环境准备(一次性)
P="$HOME/.workbuddy/binaries/python/envs/default/bin/python"
$P -m pip install -i https://pypi.tuna.tsinghua.edu.cn/simple pymobiledevice3
从手机取素材
$P -m pymobiledevice3 usbmux list # 确认设备(DeviceName / iOS / UDID)
$P scripts/iphone_scan.py --date=2026-10-04 # 只看某天;不带 --date 列全量
$P scripts/iphone_pull.py <本地目录> /DCIM/111APPLE/IMG_1984.MOV /DCIM/111APPLE/IMG_1984.HEIC ...
iphone_scan.py每行最后一段就是设备上的绝对路径。- 拉取 API 注意:
AfcService.pull(relative_src: str, dst: str)的dst是本地路径字符串,不是文件对象(传open(f,'wb')会报stat: path should be string…)。脚本已修正。
电脑本地素材:直接指定目录即可
两个拼接脚本的第一个参数就是素材目录,不关心文件来自手机还是电脑。最省事的做法:把要用的片段/照片复制到同一个文件夹,文件名前缀决定顺序(01_、02_…),然后跑脚本。
拼接成片(A–E 五个环节,各用不同脚本)
A. 保真拼接 —— concat_clips.py
全片长、按文件名自然序、硬切、零转场。
$P scripts/concat_clips.py <素材目录> -o 输出.mp4 [--size WxH] [--fill blur]
[--image-hold 3] [--zoom 1.06] [--no-zoom] [--no-images]
- 自动按文件名自然序排(
IMG_2在IMG_10前) - 画幅自动选:按真实显示尺寸的多数派,让多数段零损失(
--size可强制)。⚠️ 自动选只是兜底——先按铁律零问平台,并把另一画幅也出一份 - 静态照片按
--image-hold秒停留(默认 3s),默认带缓慢推镜(--no-zoom关) - 与视频同名的 HEIC 封面自动跳过
- 无音轨的片段自动补等长静音,保证音轨不断
- 混排时默认
--fill blur模糊铺底(pad= 黑边)
B. 编排剪辑(要"有冲击力"时用)—— impact_edit.py
掐段 + 重排序 + 静图停留 + 交叉淡化 + 首尾淡入淡出。
$P scripts/impact_edit.py <素材目录> -o 输出.mp4 [--plan plan.json]
推荐用 --plan 传片单(脚本不用动):
{
"settings": {"size": null, "fps": 30, "xfade": 0.22, "fade_in": 0.3, "fade_out": 0.6, "fill": "blur"},
"clips": [
{"file": "IMG_1989.MOV", "in": 0.30, "out": 2.30},
{"file": "PHOTO_0001.jpg", "hold": 2.5, "zoom": 1.10},
{"file": "IMG_1986.MOV", "out": 1.75}
]
}
字段:file(必填) / in(默认0) / out(默认为片尾) / hold(静图停留秒,默认 3.0)
/ zoom(静图推镜终值,默认 1.06;1.0=不动) / fill(pad|blur)。
settings.size 省略或 null → 自动按多数派选画幅。
实现=逐段归一化 → xfade 链(offset 逐段累加 前段时长 − 转场时长)+ acrossfade 链;总时长 = Σ段长 − 转场时长×(段数−1)。
C. ⚠️ 收尾:强制 CFR 重编(concat_clips.py 出片后必做)
ffmpeg -v error -y -i in.mp4 -c:v libx264 -preset medium -crf 18 -pix_fmt yuv420p -r 30 \
-c:a copy -movflags +faststart out.mp4
复检 ffprobe:r_frame_rate 应为 30/1。(impact_edit.py 内部已按 -r FPS 输出,一般不用补。)
D. 要给成片配乐 / 卡点 —— beat_plan.py
先记死一条事实:音乐生成模型不保证节拍对齐。 ChatCut(底层 Mureka)官方文档原话:
Mureka cannot guarantee exact beat, drop, lyric, or timestamp alignment … If the user requires precise beat-level sync, this must be handled as a timeline/audio edit.
所以别指望"喂视频给模型、让模型踩点"。凡是号称自动卡点的(剪映踩点、秒剪卡点),卡点都是剪辑器做的。 可控的正确姿势是反着来:先定 BPM,再把每段锁成整数拍 → 剪辑点天然落在节拍网格上,之后换哪首 BGM 都能踩。
# 单拍 = 60/BPM。BPM 100 → 单拍 0.6s;快切 2 拍=1.2s / 常规 3 拍=1.8s / 高潮 4 拍=2.4s
$P scripts/beat_plan.py --bpm 100 --beats 4,2,3,2,3,3,4,3 \
--clips IMG_1989.MOV IMG_1986.MOV IMG_1987.MOV IMG_1991.MOV \
IMG_1984.MOV IMG_1990.MOV IMG_1988.MOV IMG_1972.MOV \
--dir 实况素材_原始 -o 卡点_plan.json
# → 生成一份 xfade=0 的 plan.json,并打印每段落在第几拍、成片区间
$P scripts/impact_edit.py 实况素材_原始 --plan 卡点_plan.json -o 卡点_竖版.mp4
规则:
- 单拍时长 = 60 / BPM;每段时长 = 拍数 × 单拍。总拍数取 4 的倍数,收尾才落在 4/4 小节点上。
xfade必须 = 0。交叉淡化每处吃掉xfade秒,段起点随段数累加漂移,到第 8 段就偏出小半拍。impact_edit.py检测到xfade=0会自动走硬切路径。- 每段时长不得超过源片长。
beat_plan.py会告警并夹到片长——那一段就掉拍了。正解是减一拍,别让源片决定节奏。 - 入点默认取居中(避开起幅/落幅),照片用
hold停留(--stills指定,--hold可强制固定秒数)。 - 拿到片单后怎么配 BGM:见下「配乐三条路」。
E. 加片头封面页 —— make_cover.py
为什么必须做:微信、Finder、相册取首帧当缩略图。成片首帧如果是一块黑,整条视频在对话列表里就是"黑不溜秋"的一格,点都不想点。封面亮度是硬指标,不是审美问题。
2026-10-04 实录:用户原话「你那个剪辑的那个15秒都缺封面页,分别帮我加点封面页,不然黑不溜秋的」。 实测首帧
YAVG=0(纯黑)——片单里明明已经写了fade_in: 0.0,但没用(见下面第 ① 条)。
① 黑场元凶:fade=t=in:st=0:d=0 不是"不淡入"。
ffmpeg 的 fade 滤镜 d 默认 1 秒;传 d=0 时它不报错,而是回落到那一段很长的默认淡入。
用纯白信号实测(10fps):d=0 时 8 帧才亮到 1/3;整条滤镜不加才是恒定满亮。
→ 正解:fade_in <= 0.001 时整条 fade 滤镜都不加,用 null(视频)/ anull(音频)占位。
impact_edit.py 的 _fade() 已按此写死:
def _fade(prefix):
parts = []
if FADE_IN > 0.001:
parts.append(f"{prefix}=t=in:st=0:d={FADE_IN}")
if FADE_OUT > 0.001:
parts.append(f"{prefix}=t=out:st={fade_st:.3f}:d={FADE_OUT}")
return ",".join(parts)
# 链接处:f"[{vlab}]{VF if VF else 'null'}[vout]"
| 写法 | 首帧 YAVG |
|---|---|
| fade=t=in:st=0:d=0 | 16(半黑,缩略图照样黑) |
| 整条 fade 不加 | 235(纯白信号基线) |
| 本案例封面静图段(fade_in=0) | 117.8 / 120.6 ✅ |
② 怎么量亮度(方法本身有个坑)。
ffmpeg -v error -i x.mp4 -vf \
"signalstats,metadata=print:key=lavfi.signalstats.YAVG:file=-" -frames:v 1 -f null -
⚠️ :file=- 必须显式写。metadata=print 默认把结果写进 INFO 日志,而 -v error 会把它一起吞掉
→ 你会永远"量不到亮度"。判据:封面 YAVG ≥ 110 才算亮得起来。
③ make_cover.py:扫候选 → 挑帧 → 裁画幅 → 量亮度。
# 扫目录,按平均亮度列出所有可当封面的候选帧(src 是位置参数)
$P scripts/make_cover.py 实况素材_原始 --scan --top 12
# 出图(竖 / 横各一张)
$P scripts/make_cover.py 实况素材_原始/IMG_1987.MOV --at 2.6 \
--size 1080x1440 -o 实况素材_原始/_封面_沙滩秋千_竖.png
$P scripts/make_cover.py 实况素材_原始/IMG_1987.MOV --at 2.6 \
--size 1440x1080 --crop-bias 0.18 -o 实况素材_原始/_封面_沙滩秋千_横.png
实测候选亮度(同一批素材):
| 源 | YAVG | 取舍 | |---|---|---| | IMG_1988 @2.4s(树影日落) | 67.2 | ❌ 好看但太暗,当封面照样糊 | | IMG_1987 @2.6s(沙滩秋千) | 117.8 | ✅ 蓝天白云 + 人物动势,缩略图最跳 |
看上去美 ≠ 缩略图跳。手机相册里那格只有指甲盖大,亮度与主体大小才是决定性的。
④ 裁画幅:横版用「中心横裁」,别用模糊边。
竖素材(3:4)放进 4:3 横画布,两条路:
| 做法 | 结果 |
|---|---|
| --fill blur(正片里的默认) | 画面只占宽 56%,两侧 44% 是糊的 → 封面糊一片,白瞎 |
| 中心横裁(crop=iw:iw*3/4) | 画面铺满,主体居中 → ✅ 封面要的是"一眼看清是什么" |
→ 封面图是单帧,没有"动作连续"的顾虑,直接裁掉上下就行(正片才需要保构图完整)。--crop-bias 控制纵向取景重心(0.5=正中,越小越靠上)。
⑤ 封面帧不能与正片任何一段撞脸。
第一版封面取 IMG_1987 @1.0s,而正片第 3 段用的就是 IMG_1987 的 0.583–2.383s
→ 同一画面在 1.8s 处又出现一次,观众会觉得"卡了一下"。改取 @2.6s(人物荡到前方、姿态明显不同)后消除。
挑封面帧前先把片单读一遍,确认 --at 落在别人没用的区间。
⑥ 把封面段写进片单(静图段放第一位,fade_in 归 0)。
{
"settings": {"size": null, "fps": 30, "xfade": 0.0,
"fade_in": 0.0, "fade_out": 0.6, "fill": "blur"},
"clips": [
{"file": "_封面_沙滩秋千_竖.png", "hold": 1.8, "zoom": 1.04},
{"file": "IMG_1989.MOV", "in": 0.267, "out": 2.667}
]
}
- 封面
hold取整数拍(BPM 100 → 1.8s = 3 拍),整条拍的网格才不被打乱。 - 封面也带 1.04 的极缓推镜——1.8s 的静止图会显得"卡住",微动就活了。
fade_in: 0.0让首帧就是满亮(配合上面第 ① 条)。- 横竖两版各写一份片单(
卡点_封面_竖.json/卡点_封面_横.json),因为封面图文件名与size都不同。
⑦ 验收(两版都要过):
帧数 = 486(= 16.200s @30fps,27 拍:封面 3 拍 + 正片 24 拍)
首帧 YAVG ≥ 110 ← 不黑
第 54 帧(=1.80s)已是正片第 1 段 ← 封面段精确 54 帧,不多不少
配乐三条路(按"零成本 → 最优"排)
🔴 铁律:配乐前先问一句「音乐谁来加」
在生成视频、添加音乐之前,必须先问用户:音乐是要自己加,还是希望 WorkBuddy 帮他加。 别默认替她决定配不配乐、配什么曲。
- 音乐是主观性最强的部分。用户心里往往已经有 BGM(在抖音/视频号上听到的那首、或自己收藏的),AI 硬塞一首 → 整条要重做,比多问一句贵得多。
- 用户选**「自己加」** → 只出无配乐母版(
concat_clips.py/impact_edit.py的成片即可),并把拍网格交给他(单拍 X 秒、总 N 拍),他拿去第三方工具踩点能直接对齐。 同时主动推荐第三方工具:剪映(CapCut) 最常用——「音频 → 音乐 → 踩点 → 自动踩点」能按拍自动打点,「提取音乐」可换任意 BGM,手机端/桌面端都有; 其他可选:秒剪(微信生态,配乐长度自动匹配)、必剪(B站)、VN。 - 用户选**「帮我加」** → 才走下面三条路。
判断口诀:没问过就不要开始生成配乐。"顺手配一首"不是贴心,是替用户做决定。
| 路 | 做法 | 成本 | 适合 |
|---|---|---|---|
| ① 现成曲库卡点 | 先按拍数出片(beat_plan.py),再把成片+音乐丢进剪映「踩点」/秒剪,或在 impact_edit.py 片单里直接加一条 BGM 音轨 | 0 | 绝大多数情况——本机 ffmpeg 加 BGM 就是零成本 |
| ② 本地生成原创 | 本机已装好:ACE-Step 1.5 跑在 MLX 上,生成时用 --bpm 100 指定同一 BPM,再走 pick_cue.py → mix_cue.py(见下节) | 0 | 要原创、要离线、不想用热门 BGM |
| ③ 云端生成 | ChatCut Mureka 0.18 积分/首;Sonilo 是少见的视频原生(读画面出配乐,对齐转场与末帧);剪映「AI配乐」免费且长度自动匹配 | 积分/订阅 | 不想自己剪、要"像为这段画面写的" |
⚠️ 三条路都不改变上面的结论:拍子由剪辑侧锁死,音乐侧只负责"同 BPM"。
⚠️ 但走②时会撞上四个问题,全都实测过、都已有对策(见下节「三条实测硬事实」): 音乐没贯穿全片(模型不填满时长)、剪辑点踩空(局部相位漂)、 单调(和声不动)、验收被"音量大就是有音乐"骗过(音乐整轨为空也像正常)。
本地生成配乐(ACE-Step 1.5)—— 本机已装好,直接可用
为什么值得用:免费、离线、可指定 BPM,本机 M5 Pro 实测 DiT 走原生 MLX 加速, 8 步扩散 4s + VAE 解码 4s;15 秒曲子约 65 秒出片,48 秒曲子(batch 3)约 5 分钟。
⚠️ 时长要写长不写短:短曲子只够写一两个乐句,必然单调;且模型不会把请求时长填满(见下 ①)。
默认用 --duration 48,再从里面截一段。
安装位置(已就绪,勿重装)
| 项 | 路径 |
|---|---|
| 源码 + venv | ~/ai-tools/ace-step-1.5(.venv,Python 3.12;项目要求 3.11–3.12,3.13 不行) |
| 权重 | ~/ai-tools/ace-models(9.4GB),已软链到 <repo>/checkpoints —— 代码固定读 project_root/checkpoints |
| 生成脚本 | references/generate_bgm.py(支持 --caption / --tag / --duration) |
| 下载脚本 | references/dl_acestep_ms.py |
🔴 三条实测硬事实(2026-10-04 全部踩过,别再踩)
① 模型不会把请求的时长填满 —— 多出来的部分是纯静音。 请求 15s,它作到第 12.0s(第 21 拍) 就结束了,剩下 3 秒是 −25.5dB 的数字静音。 按 0.31s 的拍相位裁一下,静音尾巴就前移到成片第 11.7s → 最后 2.5 秒只剩海浪声。 (用户原话:"音乐没有贯彻全部视频。") → 对策:故意写长(45–60s),再从里面截一段。 48s 实测有 79 拍可用,挑 25 拍轻轻松松; 15s 的曲子只有 24 拍、末尾 4 拍静音,根本凑不出 25 拍。
② 拍相位必须在窗口内重算,不能用整首的全局相位。
生成曲的局部相位会漂:全局拟合出的相位,在某个 15 秒区间里可能正好落在反拍上。
实测同一首曲子:按全局相位取窗口,踩点比 0.72(全踩在拍缝里);改成在全曲时间轴上按 1/6 拍细搜,
同一个区间踩点比升到 1.6。
→ pick_cue.py 的做法:窗口起点不锁网格,直接在整条时间轴上细搜,每个候选各自算踩点比。
(合成信号自证:鼓点在 0/0.6/1.2… 时它选 19.199s=第 32.00 拍;鼓点在 0.3/0.9/1.5… 时它选 16.499s,
自己重新找到了鼓点。)
③ "挑一段好听的"不能只看能量 —— 要量三个彼此独立的指标。
| 指标 | 怎么量 | 判据 | 治什么 | |---|---|---|---| | 覆盖 | 逐拍 150Hz 以下能量,低于全曲中位 12dB 记为死拍 | 死拍 = 0 | 音乐断层 | | 踩点比 | 剪辑点(每拍起点)起音强度 ÷ 拍中间起音强度 | ≥ 1.3 | 卡点踩空(<1 就是全踩在拍缝里) | | 和声变化度 | 相邻小节色度向量的余弦距离 | ≥ 0.10 | "只有一个调子" |
判据全是实测标定的:用户说"单调"的那首 0.073;同一和弦循环 0.00;有真实和声进行 0.17–0.50。 光看能量曲线挑不出这些问题——踩点比 0.72 的那一段,能量曲线是漂亮的。
流程(生成 → 筛选 → 出片,一条命令收尾)
cd ~/ai-tools/ace-step-1.5
P=~/ai-tools/ace-step-1.5/.venv/bin/python
# ① 生成:写长 + batch 3 + 打 tag(多轮不要互相覆盖)
PYTORCH_MPS_HIGH_WATERMARK_RATIO=0.0 .venv/bin/python references/generate_bgm.py \
--bpm 100 --duration 48 --keyscale "A minor" --batch 3 --device mps --backend pt \
--tag tight --no-understand --caption "…见下「caption 怎么写」…" --out <目录>
# bpm / keyscale / timesignature / duration 会以 # Metas 真正送进文本编码器,不是摆设
# turbo 模型用 inference_steps=8 + shift=3.0(脚本已默认)
# ② 筛窗口 + 对齐 + 混音 + 自检(一条命令;不满意就换另一首重跑)
$P scripts/mix_cue.py --bgm <目录>/bgm_bpm100_tight_03.wav \
--video 卡点_竖版.mp4 --video 卡点_横版.mp4 \
--bpm 100 --ambient-db -14 --swell 2 --suffix _配乐
mix_cue.py 内含强制自检:先单独抽出音乐轨,确认非空且最后一拍仍有音乐,
不通过就中止、不出片。(第一版漏了这道,出了个"音乐整轨为空、只剩海浪声"的成片。)
只想看某首曲子会挑在哪一段:
$P scripts/pick_cue.py <目录>/bgm_bpm100_tight_03.wav --bpm 100 --seconds 14.506 --top 8
⚠️ 两个必须记住的坑
坑一:atrim 的 end 用的是原始时间轴。
先写 atrim=start=17.3 再写 atrim=end=14.506,后者等于"17.3 秒之后、14.5 秒之前"取交集
→ 空集,音乐整轨消失。(start=0.3094 时侥幸没炸,只因 0.3094 < 14.506。)
正解:第一刀把 start/end 一次写全,随后立刻 asetpts=N/SR/TB 归零时间戳,再做 atempo 与末尾裁切。
mix_cue.py 的 music_chain() 已按此写死。
坑二:验收要验"音乐轨",不能只验"成片音量变大"。
音乐轨为空时,成片里只剩被压低的现场原声,而音量对比也可能"看起来正常"。
正确口径:用 scripts/analyze_cue.py 打逐拍低频表,末两拍不得比中位低 8dB 以上。
caption 怎么写(直接决定"单不单调")
caption 最长 512 字符,把编排写进去。实测对照:
| 写法 | 结果 |
|---|---|
| ambient, no drums, unhurried | 和声变化度 0.03、踩点比 ≈1.0 → 就是"只有一个调子" |
| locked steady 100 BPM groove, clear kick on every beat, shaker on offbeats, no rubato, tight quantized timing | 踩点比 3.6–4.3(拍极准),但和声变化度常掉到 0.00–0.08(单一和弦) |
| 上面两条合并 + fingerpicked arpeggios with a memorable melody, strings and bass filling in, arrangement growing into a full climax | 踩点比 1.63 + 和声变化度 0.247,两个轴同时达标 ← 本技能选用的写法 |
一次多出几首(--batch 3)再按指标挑,比反复调 caption 快得多——实测 7 首里只有 1 首两项都过。
诊断工具(align_bgm.py / check_bpm.py / analyze_cue.py)
check_bpm.py—— 单首曲子的 BPM 粗估align_bgm.py—— 整首同相位情形的「周期+相位」联合拟合(比单点估准得多:单点曾误报 98.68,联合拟合为 100.015)analyze_cue.py—— 打逐拍/逐 0.25s 的分频能量表,看音乐到底在哪些秒是真的在响
判「音乐在不在」用 150Hz 以下能量:现场原声(浪、人声)主要在中高频,配乐有低频底。 注意各脚本的绝对值刻度不同(
pick_cue与analyze_cue差约 19dB 常数偏移),只看趋势与相对差,别跨脚本比绝对值。
墙内装它踩过的坑(都已解决,重装时照抄)
| 环节 | 结论 |
|---|---|
| 权重源 | ModelScope 8.26 MB/s,hf-mirror 只有 1.6 MB/s → 差 5 倍,优先 ModelScope(且是官方列出的分发渠道);仍用 hf-mirror 的 API 清单做体积交叉校验 |
| 期望体积取自 | https://hf-mirror.com/api/models/<repo>?blobs=true —— 下载走 A、校验值取自 B,防半截文件 |
| 源码 | 只有 gh-proxy.com 通(~29KB/s,8.8MB 约 5 分钟);ghfast.top / hk.gh-proxy.com 均 0KB/s |
| pypi | 清华镜像 27 MB/s;必须改写 uv.lock(--frozen 会绕过索引设置),用 python-deps-behind-gfw 技能的 mirror_lock.py |
| macOS portable 包 | files.acemusic.ai 实测 8KB/s(≈986 分钟),放弃,别浪费时间 |
| ⚠️ 别自己打自己 | 起过两个 curl 写同一个 .zip → 互相覆盖、体积卡死不动。一个文件只留一条下载线;pkill -f "<文件名>" 会连你自己刚起的那条一起杀 |
macOS 关键参数:device=mps、LM backend=pt(vllm 是 CUDA 专用)、DiT 自动走 MLX;
MPS 报 OOM 时设 PYTORCH_MPS_HIGH_WATERMARK_RATIO=0.0。
先定画幅:按"多数派的真实显示比例"
混排时没有一种画幅能都不留边,所以按占多数的那一侧选,让少数派去承担填充:
| 画布 | 横屏 4:3 段 | 竖屏 3:4 段 | |---|---|---| | 3:4 竖屏(如 1080×1440) | 上下各约 25% 留白(画面占宽 100%) | 0 损失 | | 4:3 横屏(如 1440×1080) | 0 损失 | 画面只占宽 56%,两侧共留 44% |
⚠️ 关键:竖屏 3:4 素材放进 4:3 横屏画布,损失不是 25% 而是 44%(只能按高度顶满,宽度剩 1080×(3/4)÷1440 ≈ 56%)。所以多数派是竖屏就必须选竖屏画布——这是 2026-10-04 那次翻车的直接原因。
填充一律 --fill blur(放大铺满 + 高斯模糊做底)。若填充区域本身是大面积同色(水面、天空、沙滩),模糊铺底几乎看不出接缝。
实测:8 段里 6 段是竖屏 1440×1920 → 选 1080×1440,6 段零损失,只有 2 段横屏(货轮、远船,都是天空/水面)承担模糊填充,肉眼几乎无感。
提高冲击力(按杠杆从大到小)
- 背景音乐——最大的一件事。 没有音轨,成片只能算"整理好的素材";一首曲子能让同一批画面的情绪强度翻倍,切点还能对齐节拍。(本地 ffmpeg 加 BGM 零成本;要卡点就按 D 段
beat_plan.py先锁拍数再出片;要通过 ChatCut 生成原创音乐,0.18 积分/首。零成本替代:导入自己的 mp3,或先翻内置音效库。) - "紧—松"落差。 各段时长别均匀。前几段压到 1.2 秒快切建立速度感,到高潮那段反而放到 3.5 秒不动——落差本身就是冲击力。
- 收尾落在笑点上。 用"有人"的画面收尾(自拍、彩蛋),而不是最好看的风光——风光是好看的画面,人是记得住的画面。要庄重感就反过来。
- 静景加微动:几乎静帧的段(货轮、云)加 1.0→1.08 的极缓推镜。静态照片更要加,否则 3 秒死图很闷(脚本默认
zoom: 1.06)。 - 转场别一刀切:全片同一种淡化会显得平。高潮那一刀换硬切或白闪,会"响"得多。
- 片头/片尾文字:一行地点+日期就够。
例:2026-10-04 海边黄昏实况(本技能首个案例)
iPhone 15 / iOS 26.6.2。用户分两批存了 10 个实况文件(IMG_1972、1984–1992),内容级去重后剩 8 段(IMG_1984 被存了 3 次),合计 20.6s。
真实显示尺寸(不是编码尺寸):6 段竖屏 1440×1920(1972/1984/1987/1988/1990/1991)+ 2 段横屏 1920×1440(1986 货轮、1989 远船)。
# 只需问一句"发哪个平台",然后同一份片单跑两遍
# ① 保真版:时间序、全片长
$P scripts/concat_clips.py 实况素材 -o 实况合并_全量_竖版.mp4 # → 1080x1440 / 30fps / 20.7s
$P scripts/concat_clips.py 实况素材 --size 1440x1080 -o 实况合并_全量_横版.mp4
# 各再补一遍强制 CFR 重编(见上「C. 收尾」)
# ② 冲击力版:掐段 + 叙事排序 + 交叉淡化(同一份 plan.json,只改画幅)
$P scripts/impact_edit.py 实况素材_原始 --plan 冲击力_plan.json -o 实况剪辑_冲击力版_竖版.mp4
$P scripts/impact_edit.py 实况素材_原始 --plan 冲击力_plan.json --size 1440x1080 -o 实况剪辑_冲击力版_横版.mp4
# → 两版均为 30fps / 14.5s,剪辑点与顺序完全一致,只有画布不同
本案例交付了横竖各一对,用户的结论是「两个版本都挺好,都保留」。注意:本批素材 6 竖 2 横,竖版零损失、横版有 6 段要模糊补边——但横版对"电脑看/投屏/存档"仍然有价值,所以照出、照留,把选择权交回用户。
冲击力版的叙事弧(写进 plan.json 的 clips 顺序):紫雨衣人+远船(开场)→ 货轮(拉远)→ 沙滩秋千(动起来)→ 云+远船(呼吸)→ 栈桥蹲身指水(人物动作)→ 人影剪影(情绪下沉)→ 日落树影(高潮,留最长 2.5s) → 车内自拍叼薯片(彩蛋收尾)。
卡点版(后加的,验证 D 段流程):BPM 100、拍数 4,2,3,2,3,3,4,3 = 24 拍 = 14.40s(6 小节 4/4),
xfade=0 硬切 → 实出 14.506s,与拍网格吻合。这一版是"同一批素材的又一个输出形态",
按「能合并就合并」原则并入本技能,未新建 skill。
配乐版 v1(翻车,留作反面教材):用 ACE-Step 生成 15s 配乐铺上去,用户听到两个问题—— "音乐没有贯彻全部视频""有点单调,总感觉只有一个调子"。两句都对,且都可量化:
| 用户的话 | 实测 | |---|---| | 音乐没贯彻全部视频 | 源曲第 12.0s 后是 −25.5dB 纯静音(模型没填满 15s),裁剪后落在成片 11.7–14.5s | | 只有一个调子 | 和声变化度 0.073(对照:同一和弦循环 0.00,好曲子 0.17–0.50) |
同批还发现 踩点比 0.72(剪辑点全踩在拍缝里)——这个用户没提,但同样是硬伤。
修法见「三条实测硬事实」:生成 48s 冲淡 → pick_cue.py 按三指标筛窗口(并在全曲时间轴细搜相位)
→ mix_cue.py 混音 + 强制自检。7 首候选中仅 1 首同时过"踩点比 ≥1.3"与"和声变化 ≥0.10"。
配乐版 v2(两条硬伤都修掉):换 bgm_bpm100_tight_03.wav(和声变化 0.167、踩点比 1.37),
末两拍相对窗口内中位 +8.3dB → 逐拍 27 个值全在 25–41dB,音乐贯穿到结尾 ✅。
封面版(用户第三轮反馈):「你那个剪辑的那个15秒都缺封面页,分别帮我加点封面页,不然黑不溜秋的。」
→ 量出首帧 YAVG=0:片单里 fade_in: 0.0 明明写了却没生效——fade=t=in:st=0:d=0 回落成了长淡入(见 E 段 ①)。
修完 fade + 加 3 拍封面静图段(IMG_1987 @2.6s,封面图 YAVG 117.8)后:首帧 117.8(竖)/ 120.6(横),
母版 486 帧 / 16.200s(= 封面 3 拍 + 正片 24 拍)——过程中顺带修掉 4 个帧精度 bug(踩坑段 9–11)
与 1 处跨工具口径打架(踩坑段 12)。最终交付 实况剪辑_卡点BPM100_封面配乐_竖版.mp4 / _横版.mp4(各 16.320s)。
法律科技实务工具 · 维护者陆凌燕律师(北京德恒·无锡)
微信扫一扫