下载地址
https://www.zhengdianxue.com/wp-content/uploads/skills/leyongai--lyai-rough-cut-std.zip将提示词发送给你的 AI 安装该 skills
请根据 https://skillhub.cn/install/skillhub.md,安装 @leyongai/lyai-rough-cut-std。一键粗剪:口播视频自动粗剪 skill——本地 ffmpeg 提音频 → 独立 VAD → 字词级 ASR(本地 faster-whisper)→ 时间戳校验修复(含字幕错字修正)→ Clause 切分 → L1 规则引擎(紧邻重复/前缀重来/近音改口/元话语/语气词全域清理/短距重复/自指赘语/改口链/声学浓缩口吃/停顿三层)→ L2 大模型语义判定(可降级)→ EDL keep 白名单合成 → 字幕 ASS(默认网感逐字字幕,两行往下叠)→ ffmpeg 切割重连(字幕同次编码烧入)→ 验收报告 + EDL 审片页 + 剪辑明细报告 → BGM 混音 → 封面抽帧(正脸/睁眼/嘴闭,标题自动从字幕抽)→ 把封面挂成成片第一帧出发布版。
视频文件全程不出本机,纯本地可跑通(只有字幕断句的语义审稿会调一次大模型中转,拿不到结果就自动降级,不调也能出片)。本技能被加载或用户刚安装完成时,第一动作是先用表格展示功能详细单再处理需求。当用户说"一键粗剪""粗剪这个视频""帮我剪一下""剪口播""去掉语气词停顿口误""加字幕出成片""注册""充值""积分""还能剪多少"时使用——这类口语整活默认交付"成片+BGM+字幕+封面"一整套成品,不要反问要不要 BGM/封面。
与 oral-roughcut 的区别:本 skill 不依赖任何远端剪辑服务,全链路本地执行(仅句界语义审稿走自带中转、可降级),架构为 keep 白名单 EDL。
阅读全文 (展开查看完整说明)
概述
一键粗剪(onecut)
⚡ 第一动作:先展示「功能详细单」(无条件、最先执行)
只要本技能被加载,在回复任何实质内容之前,必须先用表格展示功能详细单(保持下表的编号、列与文字,不要增删改写)。触发时机包括但不限于:
- 用户刚安装完本技能(收到安装成功提示后)→ 主动展示功能单;
- 用户首次提到粗剪 / 剪辑 / 口播 / 去语气词 / 加字幕 / 封面 / BGM / 充值 / 积分 / 余额等任何相关话题 → 先展示功能单,再处理需求;
- 用户问「这个技能能干什么 / 有什么功能」 → 直接展示功能单。
| 序号 | 功能 | 说明 |
|---|---|---|
| ① | 注册试用 | 手机号即账号,注册即送 500 积分(=成片 100 秒),自动生成专属访问令牌,装好就能用 |
| ② | 一键粗剪 | 口播视频自动去语气词 / 长停顿 / 口误 / 重复口吃,按「保留白名单」重连,出干净成片 |
| ③ | 网感字幕 | 默认逐字字幕两行往下叠,句界由大模型审稿(断句比规则准),同次编码烧进画面 |
| ④ | 封面 + BGM | 抽帧选正脸 / 睁眼 / 嘴闭的最佳帧,标题自动从字幕抽;可混 BGM;封面挂成成片第一帧出发布版 |
| ⑤ | 审片与明细 | 交付 EDL 审片页、剪辑明细报告、字幕核对表、验收报告,删了什么一目了然 |
| ⑥ | 充值 | 积分不足时充值,10 元起充,1 元 = 100 积分,微信扫码实时到账(10 元可剪 200 秒成片) |
| ⑦ | 费用与账单 | 随时查余额、今天还能剪多少分钟、充值与消费明细,有专属在线账单页可收藏 |
| ⑧ | 成片存档(可选) | 想把成片留一份到后台随时回看的,剪之前说一声就行;默认不传,视频仍只在你本机 |
| ⑨ | 寻求帮助 | 遇到任何问题:微信 leyongai | 电话 15800380196 | 官网 leyongai.com |
隐私承诺(展示功能单后紧接着说一句):视频文件全程只在本机处理,默认不上传;只有字幕断句那一步会把「文字」发给大模型审稿,且拿不到结果就自动降级成本地规则,照样出片。唯一例外是你主动选了「成片存档」,那时才会把成片传一份到后台。
展示表格后,紧接着引导一句:「你想先剪哪条片子?还没账号的话,建议先注册(注册送 500 积分,够剪一两条片子看效果)再动手。」然后按用户的回答继续,不要替用户做决定、不要未经询问就注册。
账号与计费(全程在对话内,用 oc_account.py 完成)
本技能目录 = 本 SKILL.md 所在目录,脚本在 scripts/oc_account.py。用 Bash 工具执行、用绝对路径:
python "<技能目录>/scripts/oc_account.py" <子命令> <参数...>| 用途 | 命令 | 返回 |
|---|---|---|
| 注册 / 登录 | register <11位手机号> | token(访问令牌)+ points(500)+ existed(老号=true)+ 自动写入 token.txt |
| 查余额 | balance | points、seconds_left(够剪多少秒)、当前单价 price |
| 充值下单 | recharge <金额元> | qr_url(二维码图片)、qr_file(已下载到本地的图片路径)、out_trade_no |
| 查单确认 | check <订单号> | status(paid/pending)+ balance |
| 充值/消费明细 | statement | 时间线 items(含「注册赠送」)+ points |
| 当前单价 | price | 每分钟积分、充值档位、注册赠送额度 |
说明:① 输出均为 JSON;② 令牌不用手填——register 成功后自动写进 <技能目录>/token.txt,之后剪片自动带上;③ 服务器地址可用环境变量 ONECUT_API_BASE 覆盖(默认读 config.yaml 的 llm_semantic.base_url)。
注册(用户没令牌时先问手机号)
① 用户没给手机号就先问;拿到 11 位手机号后执行 register <手机号>。② 返回 token(对用户可称「访问令牌 / API Key」)+ points(注册即送 500 积分)。③ 手机号用途说明(注册成功后紧接着主动告知,不要等用户问):「说明一下:该手机号仅用于 onecut 口播粗剪技能的账号注册与身份识别,不会用于营销推送,也不会提供给任何第三方。」并附协议链接:服务条款 https://leyongai.com/terms | 隐私政策 https://leyongai.com/privacy 。④ 令牌已自动存进 token.txt,提醒用户别把这个令牌外传(等于账号密码),并给出专属账单链接https://onecut.leyongai.com/my/report?token=<token>(只显示他自己的数据,URL 含令牌请勿外传)。
♻️ 老用户自动找回(注册即登录):手机号注册过时不会报错,接口直接返回原账号的令牌(返回里
existed: true),余额与历史记录原样保留,不会重复赠送。同一手机号永远对应同一账号。
积分不足时(提醒充值,不要直接拒绝干活)
① 剪辑日志里出现 [计费] 建会话被拒(402):积分不足… 时,先告诉用户这次少了多少积分、余额还剩多少,再把充值方式说清楚:recharge <金额元>(10 元起充,1 元 = 100 积分,10 元 = 1000 积分 = 200 秒成片)。② 下完单把二维码以图片形式给用户:用 qr_file(已下载好)直接展示,或让用户打开 qr_url 扫码。③ 用户付完款后执行 check <订单号> 确认 → status=paid 且 balance 是新的 → 再重跑剪辑。④ 没令牌 / 余额不足 / 服务挂了都不阻塞出片:技能会自动降级成规则断句,片子照出,只是断句不如模型准。所以话术是「这次先用规则断句出了片,充值后再剪会更顺」,不是「做不了」。
18 步主链(①-⑱,含 ③a/③b 两个可选旁支),三个冻结数据契约(transcript.json / clauses[] / edl.json),三条铁律:
- 大模型永远不输出时间——L2 只回 clause id,时间由脚本从 clause.s/e 取
- EDL 交给 ffmpeg 的永远是 keep 白名单——单调/不重叠/不越界由补集运算构造保证
- 没有字词级时间戳就整条失败——绝不降级到句级
用户说「帮我剪辑 / 粗剪」=交付一套成品,不是只给一个 mp4
只要触发词是「剪一下 / 粗剪 / 帮我剪 / 出个成片」这类口语整活,默认就要把下面这一套全部做完再交付,不要问「要不要 BGM」「要不要封面」——用户要的是拿起就能发的成品:
| 交付物 | 默认值 | 说明 |
|---|---|---|
| 成片 | final_封面.mp4(首帧=封面 + 字幕 + BGM) | 发布版,平台取封面取的就是这一帧;final_bgm.mp4 是不挂封面的同版 |
| 字幕 | 网感逐字字幕(fancy: pop) | 关键词自动染金黄;传统整条字幕可改 fancy: off |
| 封面 | cover.jpg(带标题)+ 候选墙 cover_candidates.jpg | 从成片里抽「正脸 + 睁眼 + 嘴闭」的一帧,标题自动从字幕抽 |
| 附带 | 字幕.srt / 字幕清单.docx / 字幕核对表.html / 剪辑明细报告.html / edl_preview.html / acceptance.json | 能核对、能改、能复现 |
一句话跑完:python scripts/run_all.py --video <原视频> --out-dir <目录>,BGM(bgm.enabled)、网感字幕(subtitle.fancy)、封面(cover.enabled)默认全开。
一键全流程(推荐)
python scripts/run_all.py --video <原视频.mp4> --out-dir <输出目录> [--topic 主题词] [--hotwords 产品名,人名] [--bgm light] [--autolevel normalize] [--no-subs]--bgm 混背景乐:内置风格 light(轻快琶音) / warm(温暖和弦垫) / tech(科技脉冲) / calm(极简氛围),或 真实音乐(素材库文件名/路径,mp3/wav,自动循环);混音输出 final_bgm.mp4,不覆盖无 BGM 底片。也可在 config.yaml 的 bgm: 段预设(style / user_file / volume_db / fade_ms)。BGM 只动音轨(画面 copy 不重编码),电平默认 -22dB RMS 并带 8dB 语音闪避不抢话。
默认已开:config.yaml 里 bgm.enabled: true + bgm.style: light(本地合成轻快垫乐,无版权),不加任何参数也会在 ⑩渲染 之后自动出 final_bgm.mp4。想换曲子改 bgm.user_file 指到你自己的音乐,想这单不要 BGM 就加 --bgm none。
用真实音乐:把 mp3/wav 放进你自己的素材库目录,在 config.yaml 的 bgm.bgm_dir 填那个目录(或直接给绝对路径),之后:
python scripts/s13_bgm.py --list # 列出素材库里的曲子
python scripts/s13_bgm.py --video final.mp4 --file "你的曲子.mp3" --out final_bgm.mp4--file 只传文件名时自动到 bgm.bgm_dir 里匹配;新增素材直接丢进该目录即可被 --list 列出。
两段式(先审后剪):
python scripts/run_all.py --video x.mp4 --out-dir d/ --plan-only # ①-⑧ 出方案不渲染
# 人工审 edl_preview.html(字幕正确率/删除点),可手改 edl.json
python scripts/run_all.py --video x.mp4 --out-dir d/ --render-only # 用确认后的 EDL 出字幕+成片+验收+明细(--video 必给)产物:final.mp4(带字幕成片)、字幕清单.docx(Word 版字幕清单+逐句对照,人直接打开)、ASR识别原文.docx(ASR 原始输出,核对识别准确率用)、字幕核对表.html、字幕.srt、subs.ass、剪辑明细报告.html(逐条删除内容+人话说明+置信度)、edl_preview.html(审片时间轴+删除溯源)、edl.json、acceptance.json、transcript.json、clauses.json、l1_cuts.json、run.log(全链路日志)。
分步调用(调试用)
| 步骤 | 脚本 | 输入 → 输出 |
|---|---|---|
| ① 音频提取 | s1_extract_audio.py --video x.mp4 --out-dir d/ | → audio.wav + meta.json(soxr 16k/mono,记 audio_offset_ms) |
| ② VAD | s2_vad.py --audio d/audio.wav --out d/vad.json | → speech[] + silences[] + mumbles[](双阈值含糊音) |
| ③ ASR | s3_asr.py --audio d/audio.wav --out d/asr_raw.json [--hotwords …] | → 词级 {i,w,s,e,p}(faster-whisper medium 本地,热词自动注入) |
| ③a ASR 原文 | make_asr_sheet.py --asr-raw … --meta … --out-dir d/ | → ASR识别原文.docx + .txt(ASR 刚出来的原样,逐句带源片时间/字数/最低 p,低置信标红,用于核对识别本身准不准) |
| ③b 补漏转写 | s3b_patch_gaps.py --audio … --asr-raw … --vad … | → 「有声无字」空档单独重听补回(默认关闭,见下) |
| ④ 校验修复 | s4_repair.py --asr-raw … --vad … --meta … --out d/transcript.json | → 干净 words[] + 字幕错字修正(correction.replacements);repair_rate>5% 转人工 |
| ⑤ Clause 切分 | s5_clauses.py --transcript … --out d/clauses.json | → clauses[](标点/停顿/超长三层切分+碎片回收,生成即冻结) |
| ⑥ L1 规则 | s6_l1_rules.py --clauses … --out d/l1_cuts.json --audio d/audio.wav | → R1/R2/R3/R4/R5(含全域+模糊拼音+含糊音+词间爆发,句尾「对吧/是吧」整块删)/R6(含浓缩口吃+重启重复+片尾重复+短语重复就近保留)/R7/R8/R9 + P 停顿四层(承担约 80% 删除量);AA 式重叠词(试试看/看看)不判口吃 |
| ⑦ L2 语义 | s7_l2_llm.py --clauses … --out d/l2_cuts.json [--topic …] | → 啰嗦/重复/零信息判定(OpenAI 兼容,失败自动降级) |
| ⑧ EDL 合成 | s8_edl.py --clauses … --l1 … --l2 … --out d/edl.json | → 合并/滤碎片/边界外扩/字边界吸附(防半个字)/帧网格对齐/转 keep 白名单 |
| ⑧b 呼吸口 | s8b_breath_floor.py --edl d/edl.json --clauses d/clauses.json | → 句间不足 breath_tail_ms + breath_head_ms(默认 60+120ms)的补静帧,写进 edl["pads"](句后)/ edl["pads_pre"](句前);按成片时间轴迭代补偿 |
| ⑧c 句界语义 | s12_semantic_boundary.py --clauses … --out d/semantic_boundary.json | → LLM 标注 complete(语义完整句)/ merge(被切碎的同一句);失败自动降级回纯规则(degraded) |
| ⑨ 字幕生成 | s11_burn_subs.py --ass-only --video … --edl … --clauses … --out d/subs.ass [--boundary d/semantic_boundary.json] | → ASS(输出时间轴,被删词不出字幕,长句自动均衡拆行;boundary 注入 LLM 句界,优先于规则词表) |
| ⑩ 渲染 | s9_render.py --video … --edl … --out final.mp4 [--subs d/subs.ass] | → <120 段 filter_complex / ≥120 段切片+concat;给了 –subs 则同一次编码烧字幕 |
| ⑪ 验收 | s10_verify.py --edl … --report acceptance.json | → keep 不变量/时长一致性(自适应容差)/删除比/低置信占比/字幕时间轴 |
| ⑫ 剪辑明细 | make_report.py --cuts l1_cuts.json --edl edl.json --out 剪辑明细报告.html | → 逐条:源时间/删除内容原文/规则/人话说明/置信度 |
| ⑬ 字幕导出 | make_subs_export.py --edl … --clauses … --l1 … --out-dir d/ --video 原视频.mp4 [--boundary d/semantic_boundary.json] | → 字幕.srt(可导入剪辑软件)+ 字幕核对表.html(字幕清单 + 源片逐句对照,删掉的字标红划线)。--video 用于取真实分辨率算每行字数,保证 SRT 断行与烧录成片一致;不传则回退 12 字/行 |
| ⑭ 字幕 Word | make_subs_docx.py --edl … --clauses … --l1 … --out-dir d/ [--boundary …] | → 字幕清单.docx(表①字幕清单 / 表②源片逐句对照,人直接打开核对用) |
| ⑮ BGM 混音 | s13_bgm.py --video final.mp4 --style light --out final_bgm.mp4 | → 内置合成垫乐(light/warm/tech/calm,numpy 生成无版权)或真实音乐(--file 文件名,自动匹配素材库;--list 看清单),电平 -22dB RMS + 8dB 语音闪避 + 头尾淡入淡出,画面 copy 只动音轨 |
| ⑯ 亮度调节 | s14_auto_level.py --video final.mp4 --out final_autolevel.mp4 [--mode normalize|histeq] [--strength 0.7] | → 直方图驱动自动调亮度:normalize=拉伸明暗动态范围(默认,同 PS 自动色阶)/histeq=直方图均衡;只重编码画面、音频 copy;run_all --autolevel <mode> 一键接线 |
| ⑰ 封面 | s15_cover.py --video auto --subs subs.ass --out-dir d/ | → cover.jpg(原分辨率,带标题)+ cover_clean.jpg(同帧不压字)+ cover_candidates.jpg(前 12 名缩略图墙,带时刻/得分/眼睛数/嘴动值)+ cover.json。--video auto 自动按「无字幕底片 > 有字幕成片」找取帧源;--time 26.8 可手动指定秒数 |
| ⑱ 封面入片 | s16_cover_intro.py --video final_bgm.mp4 --cover cover.jpg --out final_封面.mp4 [--ms 700] | → 把封面图挂成成片第一帧(前置一段静音定格,默认 700ms)。正片源按管线最后一道取(final_autolevel > final_bgm > final);先按正片参数生成同编码/同尺寸/同帧率的封面短片,再 -f concat -c copy 流拷贝拼接(几秒完事、画质不掉),时长差 >400ms 自动退回滤镜重编码兜底 |
流畅度处理(切点不跳)
粗剪最怕"咔咔跳",这条管线在渲染层做了三件事:
- 两种接缝两种走法(
plan_seams,时长按帧量化、每个接缝 +1 帧余量,累积误差 <10ms):
· 普通接缝 → 视频 xfade=fade + 音频 acrossfade,短叠化(render.xfade_ms: 110);· 跳切(两次保留段之间源画面断掉 ≥ render.jump_cut_ms,说明跳走了半句/换段)→ 三种走法(jump_mode):crossfade 叠化(默认,jump_fade_ms: 260)——柔和,但两帧叠加必然有重影;dip 淡出黑 + 淡入(不吃时长)——最显眼,不推荐;hard 硬切——接缝一格不叠,唯一没有重影的走法。配 jump_punch: 0.07(推镜)时,每处跳切翻转一次构图(1.00 ↔ 1.07),观众把这当成"切了机位",硬切就不显跳 —— 实测 47 段里 23 段推近。· 碎片段保护:任一侧段短于 500ms 的接缝一律硬切(碎句两侧画面本来不连续,133ms 叠化就能看出双人轮廓)。≥120 段时降级为逐段重编码 + concat(无转场),日志会写明。
- 音轨补齐:源文件音轨常比画面短(本机测试片短 426ms),末段音频会裁不满 →
每条音频分支 apad+atrim 补静音到精确长度,杜绝"音频提前结束、画面空转"。
- 句间呼吸口(第 8b 步
s8b_breath_floor.py):每两句之间至少停
rules.breath_tail_ms + breath_head_ms(默认 60 + 120 = 180ms;旧版 300ms 实测听感"句句硬断",V0.8.5 起改短)。breath_tail/head只能"把过长的空隙压短",源片里连着说的地方本来就只有几十毫秒,所以要做的是补静帧:· 句后那截冻前一句的末帧、句前那截冻后一句的首帧。全堆在后面 =「前一句画面卡住0.18s → 硬切 → 后一句才开口」,像人卡了一下;拆开之后后一句画面提前 0.12s 到位。· 前句末字结束处切开 keep 插 pads[i](画面 tpad=stop_mode=clone、音频 apad 补静音);后句首字处插 pads_pre[i](画面 tpad=start_mode=clone、音频 adelay + asetpts=N/SR/TB)。两者都不占源时间、不吃下一句的语音。· 呼吸口补的是纯静音 → V0.8.5 起渲染后自动垫 -56dB 粉噪(render.noise_floor_db,可关),静音段听感从"断"变成"气口"(房间底噪量级,语音段无感)。· 带呼吸口的接缝一律硬切:叠化会把冻住的那帧和下一帧搅在一起,实测 133ms 叠化吃掉 300ms 呼吸口里的 133ms,两句话又黏回去。· 落点离段首不足 80ms 时顺延到段首(不是段尾),否则那句的呼吸口会跑到几秒之后。· 补完按成片时间轴复核(measure_breath.py 同口径),不够的继续补,补偿量跨轮累计。
- 停顿分层保留:200–600ms 留 180ms,600–2000ms 留 240ms,>2000ms 大停顿留 700ms,
换段处不再被压成一刀。(V0.8.5 起句间节奏更紧,防"句句硬断"。)
呼吸口怎么量:python scripts/measure_breath.py <输出目录> —— 用词级时间戳算「后句首字 − 前句末字」在成片时间轴上的距离(静音检测测不准,呼吸口里常有气声)。
字幕质量(不吃字、不乱断行)
字幕最容易被吐槽的两件事,管线里各有对策:
- 不吃半个字(第 8 步 EDL 的「字边界吸附」):VAD 停顿边界常正好切在一个字中间,
只装到半个字的 keep 段会让成片出现「不仅了账户」(丢「开」)、「可以这么」(丢「说」)。edl.word_boundary_tol_ms: 45 之外的侵入一律吸附到字边界,被字吃光的 cut 直接撤销。
- 按词断行(第 11 步):换行点优先取 jieba 词边界,不会把「办公工具」「共享」拆开;
同时禁切英文词、禁拆叠字(有没|有、试|试看)。连接词禁停行尾:断行点绝不放「最近/然后/但是/所以」这类承接词(否则「说一说我们最近 工作的情况。」听起来像「最近」后卡顿),词表 CONJ_TAILS 可自行扩充。缺 jieba 时退化为纯字符边界,不报错。字号 subtitle.font_size: 44(0 = 按 h/22 自适应),544 宽竖屏每行约 10 字;字号变大要跟着重出 SRT/核对表/Word(每行字数变了,条数会变),make_subs_export.py --video和 make_subs_docx.py --video 都用真实分辨率算字数,别让它回落成写死的 12。
- 被停顿切碎的 clause 并回一条:
且/但这类短句被停顿切成「而且我们不仅开」+「了账户」时,
间隔 <200ms 且合计 ≤14 字、总时长 ≤3s 就合并显示。
补漏转写(可选,默认关)
整片送 ASR 时,whisper 在 30s 窗口边界/长静音后会吞句,表现为「一段有声但没字」。gap_patch.enabled: true 会把这些空档单独切片重听后补回(coverage 0.67→0.73)。代价是整片耗时 ×4~6 且偶发错字,所以默认关闭;字幕漏行严重时再开。
字幕重烧(对已有成片补/换字幕)
python scripts/s11_burn_subs.py --video final.mp4 --edl edl.json --clauses clauses.json --out final_sub.mp4样式在 config.yaml 的 subtitle: 段(字体/字号/边距/每行字数);render.burn_subs: false 或 --no-subs 可关掉一键流程里的字幕。subtitle.lead_ms: 200:字幕整体前置毫秒数——faster-whisper 的字级时间戳系统偏晚 150~300ms,不前置会出现「声音先出、字幕后到」,200ms 是口播实测舒适值(0=关闭)。
换字体(默认 SimHei,Windows 自带):只改 subtitle.font + subtitle.font_dir 两行即可,例如font: 微软雅黑 / font_dir: C:/Windows/Fonts;自己另装了中文字体就写font: 思源宋体 / font_dir: 你的字体目录(私有路径建议写进 config.local.yaml,同目录自动覆盖)。⚠️ ASS 的 Fontname 必须是字体内部的 family 名,写文件名或简称(如「新青年体」)libass 会静默回落到Arial/雅黑——所以 oc_common.resolve_font() 做别名展开(新青年体 → WenYue XinQingNianTi (Non-Commercial Use) W8),字体文件按 font_dir/新青年体.ttf 自动复制到输出目录(fontsdir=. 必须相对路径,盘符冒号解析不了)。验证是否真的生效:ffmpeg -loglevel debug ... -vf "ass=x.ass:fontsdir=." 里看 fontselect: 那行选中的文件名。
多行字幕一律「往下叠」:第一行留在单行的位置不动,第二行接在它下面——这是用户明确要求的默认行为。做法是 MarginV 每多一行减一个行高(Alignment=2 是整块靠底锚定)。两个坑:
- 行高不是 1.34×字号。
oc_common.line_pitch()读字体 OS/2 的 typo 度量(拿不到再退 hhea):
思源宋体 44px 字号的行距正好 44px(em=1.0),按 1.34 算会多减 15px,整块往上跳。
- 千万别拆成「一行一个事件」:libass 对同时刻的多个事件会做碰撞避让,第二行会被顶到第一行上面
(实测 M=110 的那行跑到 723px,比 M=154 的第一行还高)。必须一个事件里用 N 换行。网感字幕(make_fancy_subs.py)走 pos 逐字绝对定位,行 1 的 y0 只跟 margin_v 有关,多行时第 i 行 y = y0 + i*行距(行距取 1.34×字号,给弹出的放大动画留余量),天然也是往下叠。
网感字幕:用户要「关键词黄色 / 逐字蹦出 / 网感样式」时,改用 make_fancy_subs.py——断句沿用 build_events 不变,只把呈现换成「一个字块一条 Dialogue + pos 定位 + 淡入 + 放大回缩」,热词与英文数字自动染黄(--keywords 可追加)。逐字时刻取 ASR 逐字时间戳,走 src2out + xfade_mapper 与字幕同一条映射链。连续英文/数字并成一个字块定位(row_units(),宽度按整串量):逐字母 pos 会把字距算松,渲染出来是「W orkBuddy」,整词一起蹦出也更像样。
三档 --preset(默认 soft):
| 档位 | 动作 | 关键词 | 适合 |
|---|---|---|---|
soft | 122%→100%,回弹 140ms、60ms 淡入 | 金黄字 | 克制,正式内容 |
pop | 150%→100%,回弹 200ms、80ms 淡入,字号 ×1.12、描边加厚 | 金黄字 | 想要更跳、更抓眼 |
block | 同 pop 的节奏(145%/190ms) | 黄底黑字色块(BorderStyle=3) | 最网感,短视频常用 |
# ① 先出无字幕底片(关键:成片 final.mp4 已经烧过字幕,直接在上面烧会叠出两层字幕)
python scripts/s9_render.py --video 原视频.mp4 --edl edl.json --out final_无字幕.mp4
python scripts/s13_bgm.py --video final_无字幕.mp4 --out final_无字幕_bgm.mp4 # 可选,先混 BGM
# ② 出 ASS(--boundary 不传会让断句和成片差 1~2 条)
python scripts/make_fancy_subs.py --video final_无字幕_bgm.mp4 --edl edl.json
--clauses clauses.json --boundary semantic_boundary.json --preset block --out fancy.ass
# ③ 烧录(音频 copy,不重编码)
ffmpeg -i final_无字幕_bgm.mp4 -vf "ass=fancy.ass:fontsdir=." -c:v libx264 -crf 18
-preset veryfast -c:a copy -movflags +faststart final_网感.mp4--size 可覆盖字号(0=用 subtitle.font_size 再乘档位倍率)。
停留保底(解决「末字还没看清就跳没了」):逐字时刻不再共用词的时间戳,而是按字数在词内插值;每条字幕的收尾再往下延一点,让末字至少停留 hold_ms(soft 320 / pop·block 380),整句至少 min_hold_ms(1200 / 1300)。延长的部分是句间呼吸口的静音,不会盖住下一句(guard_ms: 60 把口子留住)。尾部若挤成一团(几个字共用一个词时间戳),就从首字时刻重新匀速排(mode=repace),首字不会被提前。可用 --hold / --min-step 覆盖。
封面(⑰ 自动抽帧)
python scripts/s15_cover.py --video auto --subs subs.ass --out-dir d/ # auto=自己找取帧源
python scripts/s15_cover.py --video final_无字幕_bgm.mp4 --subs subs.ass --out-dir d/ --time 26.8 # 手动指定挑帧逻辑(都是踩过坑才定下来的):
| 要躲的 | 判法 | 参数 |
|---|---|---|
| 张嘴/说话中 | 候选时刻只落在字幕条之间的停顿里(不说话=嘴一定闭着),长停顿取 0.55/0.75/0.90 三个探针 | cover.min_gap_ms: 240 |
| 嘴还在动 | 嘴部 ROI 在 ±45ms 的帧差,动得越多越像话说一半 | ≥5.0 直接否决、≥1.5 扣分 |
| 微微张嘴 | 口腔暗部占比(唇缝一开就出阴影),做连续分不是阈值 | dark > 0.028 否决 |
| 闭眼/眨眼 | 两个眼部级联(带眼镜版 + 普通版)都跑,合并去重后要求左右各一只 | cover.require_eyes: 2 |
| 侧脸/多脸 | Haar 正脸级联必须只检出一张脸,且宽高比 0.62~1.05、水平偏移小 | — |
| 糊 | 脸部区域 Laplacian 方差 | 越高越好 |
产出 cover.jpg(原分辨率 JPEG q95,带标题)、cover_clean.jpg(同一帧不压字)、cover_candidates.jpg(前 12 名缩略图墙,带时刻/得分/眼睛数/嘴动值,人工复核用)、cover.json(选中时刻、全部指标、标题与出处)。日志形如[15] 封面:26.79s(得分 83.9,眼睛 2,嘴动 3.54,候选 22 帧 / 合格 2) +[15] 标题:客户喜欢的 / 而不是我喜欢的(取自「主要是客户喜欢而不是我自己喜欢的」)。一个都「合格」时逐级放宽(有脸+双眼 → 有脸 → 全部),不会失败;--time 可强行指定秒数。
封面标题(自动从字幕里抽,不是硬编)
derive_title() 的思路:口播稿里本来就有一句最抓人的话,抽出来比造一句更贴内容、也不会胡说。分句前先把字幕条拼回连续文本(字幕按长度硬切,一条常常只有半句),遇到「而/但/和」这类悬空词尾先并回去——否则「客户喜欢而|不是我自己喜欢的」会被劈成两条,最值钱的对比句就丢了。
| 优先级 | 句式 | 分 |
|---|---|---|
| ① | 对比句「A 而不是 B」 | +14(口播的情绪落点,几乎总在这里) |
| ② | 认知反转「我发现/我才想明白/原来」+ 第一人称 | +5 |
| ③ | 结论句「所以/但是/结果」开头 | +3 |
| — | 命中 TF-IDF 关键词 / 长度 8~18 字 | 各 +2 |
| — | 元话语(今天/这条视频/大家说对不对) | −6 |
抽到后还会做两步收口:主要是 这类起头虚词削掉;对比句把「我自己」收成「我」,前半截补个「的」,于是「主要是客户喜欢而不是我自己喜欢的」→ 「客户喜欢的」/「而不是我喜欢的」。
排版:标题压在中下部(块中心落在画面高度 70%,正好是胸口一带),人脸完整露出来——这是口播封面最常见的摆法,不要为了躲脸把标题一路缩到头顶那点空隙里(那样字小得没分量)。可读性靠标题带:文字后面垫一条横贯画面的半透明黑带(上下边缘 2% 高度做软过渡),字压在人体/货架这种花背景上也读得清;不再用「从画面顶端往下压渐变」的老办法——位置挪到中下部以后,顶端渐变根本够不到字。
| 参数 | 默认 | 说明 |
|---|---|---|
cover.title | true | 总开关;--no-title 等价于关 |
cover.title_pos | 0.70 | 标题块垂直中心占画面高度的比例:0.5=正中 / 0.70=中下部(默认)/ 0.78=更靠下;写 head 才退回「让到头顶以上、绝不压脸」的老排版(--title-pos 可临时覆盖) |
cover.title_font | 新青年体 | 海报粗黑标题字;也可 思源宋体 / SimHei / 微软雅黑 / 等线 |
cover.title_style | goldfirst | goldfirst=首行金黄、其余白(默认)/ twotone=首行白末行金黄 / gradient=白→金渐变 / neon=黄字红边发光(--title-style 可临时覆盖) |
cover.title_scrim | 130 | 标题后面那条暗带的黑度 0~255:越大越黑越压背景,太大会显脏、太小会糊字(老值是 158,现默认 130 淡一档;--title-scrim 可临时覆盖) |
--title "…" | — | 手写覆盖;| 手动断行,例:--title "客户喜欢的|而不是我喜欢的" |
--title-lines 1 | 自动 | 强制压成一行 |
标题字体/样式/位置不用重跑抽帧:--time 指定同一秒数即可秒出对比。配置里 title/title_font/title_style/title_pos/title_scrim 是扁平几个键——config 解析器只认两层,写成嵌套字典会把 enabled 塌到 cover 那一层去。
封面入片(⑱ 把封面挂成第一帧)
封面不是一个单独的 jpg,而是成片的第一帧。平台(抖音/视频号/小红书/公众号)取封面基本取首帧,不挂进片子的话,发出去封面常常变成某句半张嘴的画面——白做。
python scripts/s16_cover_intro.py --video final_bgm.mp4 --cover cover.jpg --out final_封面.mp4 --ms 700| 环节 | 做法 | 为什么 |
|---|---|---|
| 长度 | 前置一段静音定格,默认 700ms(cover.intro_ms) | 短到不耽误完播,长到平台能抓到这一帧 |
| 声音 | 这段是 anullsrc 真静音(实测 −91dB) | 不能有 BGM 突然响起,等于开场噪一声 |
| 拼接 | 封面短片按正片参数(同尺寸/帧率/像素格式/编码器)生成,-f concat -c copy 流拷贝 | 只重编码这 0.7s,整片一帧不重编,几秒出片、画质不掉 |
| 兜底 | 拼接后时长偏差 >400ms → 自动退回 concat 滤镜整体重编码 | 帧率/时基对不上时流拷贝会出坏片,宁可慢一点 |
| 顺序 | 必须排在 ⑩渲染之后(⑱ 在链尾) | 片里字幕已经烧死,前置定格不影响任何字幕时间轴;反过来在渲染前插封面,字幕会整体错位 |
验收(跑完自己抽三帧看):首帧应与 cover.jpg 像素级一致、0.05s 与 0.65s 两帧几乎相同(整段定格)、0.80s 与 0.65s 明显不同(已经进正片)。cover.intro_ms: 0 则只出静态封面图、不挂进片子。
依赖:OpenCV 的 CascadeClassifier(新版 cv2 5.x 已移除)。装了 4.x 的话把它的目录填到cover.opencv_dir(或环境变量 ONECUT_OPENCV_DIR),脚本会插到 sys.path 最前面;级联 xml 已随包放在 assets/models/。不装也能出封面,只是不做人脸/睁眼筛选,直接抽帧压标题。中文路径写图会静默失败,所以统一走 imencode + 二进制写。标题排版用 PIL(ImageFont 读 TTF,imencode 之外全在 RGB 里做),不依赖 cv2 的中文能力。
成片存档(可选,默认不传)
默认绝不传视频。 只有用户明确说「传一份到后台」,才在 run_all 命令上加 --share-clip。
剪之前问一句(不要替用户做决定):
要不要把这版成片传一份到后台存档?换电脑也能回看。会离开你的电脑,默认不传。
| 情况 | 怎么做 |
|---|---|
| 用户同意 | run_all.py ... --share-clip,剪完自动传,日志出现 [成片] 已上传,后台编号 N |
| 用户不同意 / 没说 | 什么都不加,视频不出本机 |
| 已经剪完才想传 | python "<技能目录>/scripts/oc_share.py" --video "out/final_封面.mp4";要挂到那笔用量上再补 --session <会话id>(run.log 里有) |
| 想以后默认都传 | config.yaml 的 share.enabled: true(必须用户同意过才改) |
限制:单部超过 share.max_mb(默认 300MB)不传;后台保留天数由服务端定(默认 30 天,到期自动删)。
L2 启用(可选)
默认关闭。启用条件(同时满足):
config.yaml里llm.enabled: true- 有可用的访问令牌(推荐放进技能目录的
token.txt,也可填config.yaml的api_key或设环境变量)
模型侧硬约束已内置:只回 {"drop":[id]}、越界整条丢弃、删头尾 2 个 clause、单次删除 ≤25%、temperature 0.1。
智能断句怎么用(按分钟计费)
第 ⑧c 句界语义(llm_semantic)和第 ⑦ L2 判定(llm)都通过 OpenAI 兼容接口调大模型。默认 base_url 指向官方中转 https://onecut.leyongai.com/v1,厂商 key 只在服务器上,技能包里只放一个你自己的访问令牌。
拿令牌(一句话搞定,别让用户去改文件)
首选:问用户要 11 位手机号 → 跑 oc_account.py register <手机号> → 令牌自动写进 token.txt,用户什么都不用配。注册即送 500 积分(约 100 秒成片),够先看一两条效果。
如果用户已经自己在网页上注册过(https://onecut.leyongai.com/account),把令牌发在对话里,直接帮他写进 <技能目录>/token.txt(一行,别加引号别加说明文字)——不要去改 config.yaml、也不要让他配系统环境变量。
令牌的三种存放方式(配一次两个 llm 段共用,优先级从高到低):
| 方式 | 怎么做 | 适合谁 |
|---|---|---|
① token.txt(默认) | 技能目录下的 token.txt,一行令牌;register 会自动写 | 所有人 |
② config.yaml | 填 llm_semantic.api_key(llm.api_key 留空会自动共用) | 习惯改配置文件的人 |
| ③ 环境变量 | export ONECUT_TOKEN=... | 开发者;Windows 的 setx 要重开终端才生效 |
给 AI 的约定:优先级是 config.api_key > token.txt > 环境变量;技能目录就是本文件所在目录,所以令牌写到「和 SKILL.md 同一层的 token.txt」即可,不用管绝对路径。
费用
| 项 | 说明 |
|---|---|
| ① 注册赠送 | 新用户注册一次性送 500 积分(=成片 100 秒),账单里能看到这一条 |
| ② 单价 | 5 积分/秒(=300 积分/分钟=3 元/分钟,1 元=100 积分);随时查 https://onecut.leyongai.com/api/price |
| ③ 计费口径 | 按成片时长扣:开始剪按素材时长预扣,剪完按成片真实时长多退少补 |
| ④ 只有这一套收费 | 不设免费额度、不设每日免费次数,扣的就是积分,没有第二种计费方式 |
| ⑤ 没产生调用 | 建了会话但一次没调模型(片子中途失败)→ 全额退回 |
| ⑥ 积分用完 | 字幕断句自动降级走规则版,片子照出,只是句号位置不如模型判得准 |
硬约束
- 令牌无效 / 余额不足 / 服务挂了 → 自动降级回规则断句,不影响出片
- 想完全绕开中转自己付账:把
base_url改成https://api.deepseek.com,
再 export ONECUT_TOKEN=sk-你自己的DeepSeek密钥(此时不建会话、不扣积分)
降级路径
| 失效点 | 行为 |
|---|---|
| L2 超时/报错/越界 | 只用 L1 出片,不阻塞 |
| VAD 底噪失效(覆盖>95% 且 silences<3) | 停顿改走 clause 字间隔判据,标 low_confidence |
| ASR 无字词级时间戳 | 整条失败(铁律三),提示换 ASR |
| repair_rate > 5% | 整条转人工(exit 2) |
| 音频包络不可用(缺 numpy/读 wav 失败) | 跳过浓缩口吃修剪与词间爆发检测,其余规则照跑 |
| 中文字体缺失(font_dir 找不到) | 字幕烧录跳过并告警,成片照出 |
| ffmpeg 单段失败 | 重试 3 次后跳过该段并告警 |
| 缺 python-docx | ⑬/⑭ 的 Word 导出跳过并提示,SRT/HTML/成片照出 |
| 缺 jieba | 字幕退化为字符边界断行(仍不切英文词) |
| fast_preview 在非 Windows | 自动从 h264_mf 退回 libx264 veryfast |
日志
全链路输出同时写 <out-dir>/run.log(run_all 用 Popen 逐行 tee),崩溃后能定位到具体是哪一步、哪条 ffmpeg 命令。
维护者本地另有回归测试脚本(发布包里不带):改判据后跑一遍,确认
L1 规则夹具、音轨偏移换算、句界语义豁免都没退化。
环境依赖
- Python 3.10+:
faster-whisper(本机 medium 缓存已就绪,自动 HF_HUB_OFFLINE)、numpy(音频包络:浓缩口吃/爆发检测)、opencc(强制简体);可选pypinyin(R3/R9 近音判据,缺省退化为字面编辑距离)、python-docx(⑬/⑭ Word 导出,缺省跳过并提示)、jieba(字幕按词断行,缺省退化为字符边界) - ffmpeg ≥ 6(含 afade/concat/ass);按 config
audio.ffmpeg_bin→ PATH → 常见安装位置自动探测 - 中文字体:
config.yaml subtitle.font_dir(默认C:/Windows/Fonts,用 SimHei)
参数
全部阈值集中在 config.yaml,调参不要改代码。