← 返回 Skill 列表
extension
分类: 其它API Key 暂未确认

手机/电脑素材 → 拼接成片(含照片停留)

把**手机(USB 直连 iPhone)或电脑**上的实况/视频/照片取到一起,拼成一条成片:要么保真拼接(时间序、原声、零转场),要么编排成**有冲击力的短片**(掐段、重排叙事序、交叉淡化),也可**按 BPM 卡点**(锁定每段拍数、剪辑点落拍),还能**用 ACE-Step 本地生成配乐并精确对齐到剪辑拍点**。混在里面的**静态照片会按指定秒数停留**并可加缓慢推镜。触发场景:用户说「手机已经连电脑了」「从手机里找今天拍的照片/实况」「实况拼成长视频」「这些片段合成一条」「帮我剪得更有冲击力」「视频里放的照片也停几秒」「把照片也剪进去」「配个音乐」「生成一段音乐」「我自己加音乐」「音乐我来配」「不用你配乐」「按节奏剪」「卡点」「踩点」「音乐没贯彻全片」「后面没音乐了」「音乐太单调」「只有一个调子」「加个封面」「片头封面」「封面页」「黑不溜秋的」「首帧是黑的」「缩略图是黑的」。核心事实:iPhone 实况=IMG_xxxx.HEIC(封面) + 同名 IMG_xxxx.MOV(3秒含声),只拷 HEIC 就只剩静态图。⚠️ 三条铁律:① 只用纯 Python 的 pymobiledevice3 直读 /DCIM,绝不走 Homebrew(本机 brew 在沙箱内必败);② 判朝向必须用**真实显示尺寸**(算入旋转矩阵),用编码尺寸会把竖屏全判成横屏;③ **出片前先问用户发哪个平台,默认横竖两版都出、都保留**。⚠️ 配乐要点:**动手配乐前必须先问用户「音乐您自己加,还是我帮您加」,别默认代劳**——用户自己加就只出无配乐母版并**推荐剪映等第三方工具**(剪映/秒剪/必剪),并告知拍网格便于踩点。**音乐生成模型不保证节拍对齐**,对齐只能由剪辑侧做——先定 BPM,用 beat_plan.py 把每段锁成整数拍、xfade=0,再让配乐同 BPM。⚠️ 本地生成的曲子有四个必检项,全部实测踩过:① **模型不会填满请求时长**(要 15s,它第 12s 后就补静音)→ **必须生成 45–60s 再截一段**;② **局部相位会漂**,拍相位要在窗口内重算(窗口起点在全曲时间轴上细搜),否则剪辑点全踩在拍缝里;③ 「单调」要量**和声变化度**(相邻小节色度余弦距离,≥0.10),光看能量曲线看不出来;④ 验收必须**单独验音乐轨**(逐拍低频,末两拍不低于中位 8dB),"成片音量变大"会被"音乐整轨为空"骗过。工具:`pick_cue.py`(筛窗口+三指标)→ `mix_cue.py`(对齐+混音+强制自检)。⚠️ `atrim` 的 `end` 用原始时间轴,`atrim=start=X,atrim=end=Y<X` 会得到空集、音乐整轨消失。⚠️ **片头封面页**要点:首帧亮度是硬指标(量 `signalstats` 的 `YAVG`,建议 ≥110),**黑缩略图的元凶是 `fade=t=in:st=0:d=0`**——`d=0` **不会禁用淡入**,ffmpeg 会回落到一段很长的默认淡入,正解是 fade_in 为 0 时**整条 fade 滤镜都不加**(用 `null`/`anull`);静图段必须 `-frames:v` 钉死帧数(只用 `-t` 会少输出 1 帧);横版封面用**中心横裁**别用模糊边(模糊边会吃掉 44% 宽度);封面帧不能与正片任何一段撞脸。工具:`make_cover.py`(`--scan` 扫候选帧按亮度排序 → 挑帧裁画幅出图)。

person作者: user_b8af0005hubcommunity

手机/电脑素材 → 拼接成片

何时用

  • 手机插在电脑上,要从手机直接把照片 / 实况 / 视频拉到 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)。


其余踩过的坑

  1. 判断手机在不在:ioreg -p IOUSB -w0 | grep -i iphone。⚠️ system_profiler SPUSBDataType 在沙箱内恒返回空,不可信。设备随插拔消失/重现,别一看到空就下结论。
  2. 实况 = 两个文件:IMG_xxxx.HEIC(封面)+ 同名 IMG_xxxx.MOV(含声音)。手机里照片按批次分目录,/DCIM/ 下编号最大的(如 111APPLE)通常最新。
  3. macOS 没有 timeout 命令,脚本里别用。
  4. zsh 不会对未加引号的变量做词分割。AS="/a /b"; cmd $AS 会把整串当一个参数(bash 才会分开)→ 报 GET_FILE_INFO failed 之类的怪错。多文件要么写数组 arr=(...); cmd "$arr[@]",要么 ${=AS},要么逐个列出。
  5. concat + -c copy 出来的文件会带坏标签(time_base=1/15360 / r_frame_rate=120/1),部分播放器(含微信)会误判帧率 → 收尾必须重编一遍(见下)。
  6. 段长必须用 -frames:v N 钉死,不能只靠 -t(2026-10-04 实测)。 -ss input seek 时首帧 PTS 往往不为 0,-t 1.8 会少输出 1 帧(30fps 下 1.8s 段只得 53 帧)。 正解:-frames:v int(round(dur*fps)) 钉死帧数,-t 只给余量(dur+0.1)。 这 33ms 会顺着 concat 转移到之后所有剪辑点上——卡点成片对帧敏感,别省这一步。
  7. 硬切路径刻意不加 -r FPS。 concat filter 遇到非整数帧的段会补帧:实测 9 段凭空多出 6 帧(0.2s),把整条拍网格推偏。 对照实验:concat demuxer + copy = 483 帧(精确)|concat filter = 489 帧|concat filter 去掉 -r = 483 帧。 → 硬切路径(XFADE <= 0.001)不加 -r;只有 xfade 路径(本身会改帧率)才加。
  8. 静图段(still_segment)是同一个坑。 -loop 1 ... -t hold 也少 1 帧(hold=1.8@30fps 只得 53 帧)。正解:视频 -frames:v N + 音频 -t N/fps,两边等长。 单测:输出正好 54 帧 / 1.800000s。
  9. 跨工具的判据口径必须统一,否则"筛选觉得好、自检却拦下"。 pick_cue.py 与 mix_cue.py 判「结尾还有没有音乐」必须同一口径:都用「末两拍均值 vs 窗口内中位,容差 12dB」。 踩过的坑:mix_cue 曾用「末两拍最小值 vs 全曲中位」+ 容差 8dB → 把正常的切分空拍(单拍掉 18–23dB 属正常重音) 误判成"音乐消失",拦下一首其实合格的曲子(末两拍 22/39:均值只低 8.3,最小值却低 15)。真正的病灶是整段静音 (末两拍 −4dB,相对中位低 42dB)→ 再补一条单拍硬线 25dB 兜底。
  10. 量切点别用 -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 段横屏(货轮、远船,都是天空/水面)承担模糊填充,肉眼几乎无感。


提高冲击力(按杠杆从大到小)

  1. 背景音乐——最大的一件事。 没有音轨,成片只能算"整理好的素材";一首曲子能让同一批画面的情绪强度翻倍,切点还能对齐节拍。(本地 ffmpeg 加 BGM 零成本;要卡点就按 D 段 beat_plan.py 先锁拍数再出片;要通过 ChatCut 生成原创音乐,0.18 积分/首。零成本替代:导入自己的 mp3,或先翻内置音效库。)
  2. "紧—松"落差。 各段时长别均匀。前几段压到 1.2 秒快切建立速度感,到高潮那段反而放到 3.5 秒不动——落差本身就是冲击力。
  3. 收尾落在笑点上。 用"有人"的画面收尾(自拍、彩蛋),而不是最好看的风光——风光是好看的画面,人是记得住的画面。要庄重感就反过来。
  4. 静景加微动:几乎静帧的段(货轮、云)加 1.0→1.08 的极缓推镜。静态照片更要加,否则 3 秒死图很闷(脚本默认 zoom: 1.06)。
  5. 转场别一刀切:全片同一种淡化会显得平。高潮那一刀换硬切或白闪,会"响"得多。
  6. 片头/片尾文字:一行地点+日期就够。

例: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)。


法律科技实务工具 · 维护者陆凌燕律师(北京德恒·无锡)