公开技术文档 · 整理于 2026-10-01

门店短视频自动化生成系统 · 技术规格说明(公开版)

本文是「门店短视频自动化生成系统」的公开技术文档,描述其架构、流水线、关键算法与实现坑位。 目标读者:想了解或复现该系统的工程师、产品与技术决策者。 设计原则:读完应能照此从零实现一套功能等价、产物兼容的系统。 全文不含任何密钥、凭据或内网地址;涉及敏感配置(DeepSeek Key / rclone 凭据)均以外部环境变量 / 配置文件方式引用,不在此落明文。


0. 系统一句话定位

给定「一套门店素材(视频/图片)+ 一支 BGM + 一份品牌档案」,自动产出竖屏 1080×1920 / 30fps 卡点短视频,并派生抖音/视频号/小红书/B站/门店屏多平台版本。

系统由三层组成: - 引擎层(Python,品牌无关,读配置):素材建库 → 节拍分析 → 分镜编排 → AI 字幕 → 装配渲染 → 质检 → 指标 → 多平台派生。 - 调度层(零依赖 Node workbench/):任务队列 + 实时进度(SSE)+ 取消 + 远程任务拉取 + 成片推送。 - 触发层:浏览器工作台 UI(workbench/index.html)+ 远程 API(业务平台 /api)。


1. 总体架构与数据流

[素材库 library/<client>/ + library/_shared/]
        │  scan.py ①
        ▼
 library.json(索引 + 缩略图)  ──┐
                                  │  plan.py ④    beats.json(BGM 节拍表)── beats.py ③
        ┌─────────────────────────┘
        ▼
 edl.json(剪辑决策表 / 人工卡口)
        │  caption.py ⑤  → work/<project>/lines.txt(AI 逐镜台词)
        ▼
 render.py ⑥  → output/<project>_master.mp4
   ├─ sfx.py ⑧-b   按切点叠加卡点音效轨
   ├─ subtitle.py ⑧-c  生成 ASS 烧录字幕
   └─ 混音:BGM + 响度标准化(loudnorm) + 配音闪避(sidechaincompress) + 末级限幅
        │  qc.py ⑦  → work/_qc/<file>.qc.json(9 项门禁)
        │  metrics.py ⑨  → work/metrics.jsonl(追加一行)
        ▼
 derive.py ⑩  → output/derived/<project>_<platform>.mp4
        │
        ▼
 成品(控制台预览 / 推送业务平台)

调度层 autocut.py 把 ①③④⑤⑥⑦ 串成一键流程(② 编号在原始设计里预留,实际未单独成模块,节拍分析即 ③)。


2. 目录结构规范(重建时必须创建)

_autocut/
├── autocut.py            # 总控编排(①→③→④→⑤→⑥→⑦ + 指标)
├── scripts/
│   ├── _common.py        # 路径常量 / 配置加载 / ffprobe / 命名校验 / JSON IO
│   ├── scan.py           # ① 素材建库
│   ├── beats.py          # ③ 节拍分析(librosa)
│   ├── plan.py           # ④ 分镜编排
│   ├── caption.py         # ⑤ AI 字幕脚本(调 DeepSeek)
│   ├── llm.py            # DeepSeek 零依赖封装
│   ├── render.py         # ⑥ 装配出片
│   ├── sfx.py            # ⑧-b 卡点音效轨
│   ├── subtitle.py       # ⑧-c ASS 生成 + 烧录滤镜串
│   ├── qc.py            # ⑦ 质检门禁
│   ├── metrics.py         # ⑨ 指标记录
│   ├── derive.py         # ⑩ 多平台派生
│   ├── make_sfx.py       # ⑧-a 程序化合成音效库
│   ├── remote.sh         # 远程同步变量(被 sync-to-media.sh source)
│   └── sync-to-media.sh  # 成片/索引推送到业务平台
├── configs/
│   ├── platforms.yaml    # 通用基线配置(母版制式/音频/字幕/QC/平台规格)
│   └── remote.yaml       # rclone 远程目标变量(RCLONE_REMOTE / SITE_ROOT / DOMAIN)
├── profiles/
│   ├── _template.yaml    # 客户档案模板(照抄改)
│   └── <client>.yaml     # 每个客户一份(moen 等)
├── library/
│   ├── _shared/          # 共享素材:audio/bgm/、audio/sfx/、image/、video/
│   └── <client>/         # 客户私有素材:video/ image/ audio/bgm/ + index/library.json
├── work/                 # 工作区(产物中间文件,可重建)
│   ├── beats/  edl/  <project>/parts  <project>/sfx.wav  <project>/subtitle.ass
│   ├── _qc/              # 质检报告
│   ├── tasks/            # 远程拉取的任务落地 JSON
│   ├── metrics.jsonl     # 指标流水
│   ├── jobs.json         # 调度队列持久化
│   └── pipeline.alert.log
├── output/               # 母版成品 + derived/ 多平台版本
├── workbench/            # 调度层(见 §8)
├── templates/            # 预留
├── bin/rclone.exe + rclone.conf   # 同步工具(不入 git,单独保管)
└── .env                  # DEEPSEEK_API_KEY(明文,不入 git,单独保管)

关键路径常量(scripts/_common.py):ROOT=仓库根;CONFIG_DIR=configs/;PROFILE_DIR=profiles/;LIBRARY_DIR=library/;WORK_DIR=work/;OUTPUT_DIR=output/;TEMPLATE_DIR=templates/。


3. 环境依赖(重建前确认)

组件 版本/要求 说明
Python 3.x(建议 venv) 引擎全部脚本经 Python venv 运行;Node 调度层通过 AUTOCUT_PY 环境变量指向该 venv 的 python.exe(缺省回退到内置路径)
第三方包 librosa numpy Pillow pyyaml librosa 用于节拍分析;numpy 用于音效与首帧检测;Pillow 仅首帧标准差
ffmpeg 8.x(实测 8.1.1) 必须 ffprobe + ffmpeg 在 PATH;用到 ebur128/blackdetect/silencedetect/loudnorm/sidechaincompress 等滤镜
Node.js 内置模块即可(零依赖) 调度层只用 http/fs/path/child_process/os,不装 npm 包
DeepSeek Key sk-... 仅 ⑤ 字幕需要;缺失则降级跳过(出片不阻塞),通过环境变量或 .env 注入
rclone bin/rclone.exe + bin/rclone.conf 仅推送平台用;缺失则本机出片不受影响

Python venv 路径注入:workbench/lib/runner.js 中 PY = process.env.AUTOCUT_PY || '<内置 venv>/Scripts/python.exe',部署时通过环境变量指定,避免硬编码本机路径。


4. 数据结构 / 契约(产物 schema)

4.1 library/<client>/index/library.json(scan.py 产出)

{
  "client": "moen", "display_name": "摩恩卫浴(MOEN)",
  "generated_at": "ISO8601", "root": "<仓库绝对路径>",
  "counts": {"video":0,"image":0,"audio":0}, "total": N,
  "items": [
    {
      "id": "<文件名无扩展>", "type": "video|image|audio",
      "scope": "client|shared",
      "rel_path": "library/moen/video/xxx.mp4",   // 相对 ROOT,跨机同步用
      "local_path": "<绝对路径>",
      "thumb": "library/moen/index/thumbs/<md5前16>.jpg",
      "naming_ok": true, "naming_reason": "",
      "sync": "local_only",
      "duration": 12.3, "width":854,"height":480,"fps":29.97,
      "size": 1234567, "vcodec":"h264","has_audio":true,
      "sample_rate":48000,"channels":2
    }
  ]
}

4.2 work/beats/<bgm>.beats.json(beats.py 产出)

{
  "audio":"<BGM绝对路径>", "generated_at":"...", "duration":60.0,
  "sr":22050, "bpm":76.5, "beat_count":N,
  "beats":[{"t":0.0,"strength":0.9,"is_downbeat":true}, ...],  // 每拍
  "onset_count":N, "onsets":[...],
  "sections":[{"start":0,"end":4,"energy":0.9,"label":"chorus"}],
  "highlight":{"start":16.0,"end":20.0},                         // 能量最高段
  "source_size":123456, "source_mtime":1.7e9, "source_segment":0  // 缓存指纹
}

4.3 work/edl/<project>.edl.json(plan.py 产出,人工卡口)

{
  "client":"moen","project":"moen_20260930","created_at":"...",
  "fps":30,"size":[1080,1920],"target_duration":15,
  "window":{"start":16.0,"end":31.0},        // BGM 上的取段窗口
  "bgm":{"path":"<绝对路径>","bpm":76.5},
  "shot_count":18,"total_duration":15.2,
  "timeline":[
    {"shot":1,"src":"rel_path","abs_src":"<绝对>","type":"video|image",
     "src_in":0.0,"dur":0.79,"at":16.0,         // at = BGM 绝对秒数(含窗口起点)
     "transition":"cut","cue":"强拍|弱拍","strength":0.9,
     "subtitle":"可选逐镜台词"}
  ]
}

4.4 work/_qc/<file>.qc.json(qc.py 产出)

{"file","checked_at","passed":9,"total":9,"verdict":"PASS|FAIL","metrics":{...},"checks":[{"item","ok","detail"}]}。9 个 item:分辨率/帧率/时长/音轨/响度/真峰值/黑帧/静音/首帧非纯色。

4.5 work/metrics.jsonl(metrics.py 追加,每行一条 JSON)

{ts, project, client, shots, duration_s, elapsed_s, ok, qc:{item:bool}, bgm, picked, voice, caption}。

4.6 work/jobs.json(queue.js 持久化,原子写)

[{id, params, status, stage, pct, error, started_at, finished_at, output}]。


5. 流水线各阶段实现规范

① scan.py —— 素材建库

③ beats.py —— 节拍分析

④ plan.py —— 分镜编排(核心算法)

⑤ caption.py —— AI 字幕脚本

llm.py —— DeepSeek 封装(零依赖)

⑥ render.py —— 装配出片(音频链路核心)

流程:逐镜渲染(统一规格)→ concat 拼接 → 混 BGM+响度标准化 → 成品。

⑧-b sfx.py —— 卡点音效轨

⑧-c subtitle.py —— ASS 生成(躲 SRT 缩放坑)

⑦ qc.py —— 9 项门禁

  1. 分辨率 1080x1920 2. 帧率 30 3. 时长(有 edl 比 total_duration±0.5s;无 edl 比 qc.duration_s 硬限)4. 音轨存在 5. 响度 ebur128 ≈ -14±1 LUFS 6. 真峰值 ≤ -1 dBTP 7. 黑帧 blackdetect 8. 静音 silencedetect 9. 首帧非纯色(抽首帧算亮度 std>3)。
  2. 任一不过 → 退出码 1 + 写 pipeline.alert.log(无人值守留痕)。
  3. 时长比对语义:质检只验「产物是否符合设计(edl.total_duration)」,不验「设计是否符合用户原始意图」(BGM 只有 16s 片子就该 16s,不该 FAIL)——后者是编排阶段职责。

⑨ metrics.py —— 指标

⑩ derive.py —— 多平台派生

make_sfx.py —— 程序化音效库(可选)


6. 配置 schema

6.1 configs/platforms.yaml(通用基线,全部字段)

见源码;重点字段与保留未接字段(重做时别浪费时间接,但别删): - master:resolution/fps/vcodec/crf/preset/pix_fmt/audio_codec/bitrate/rate。 - audio_baseline:loudness_lufs=-14, true_peak_dbtp=-1, bgm_highpass_hz=30, voice_highpass_hz=80, loudness_lra=9, bgm_prechain="acompressor...,alimiter=..."(压 BGM 峰值因子,否则 loudnorm 到不了 -14)、limiter_sample_peak=0.794(样本峰值留余量覆盖 intersample peak + AAC 过冲)。 - sfx:enabled/gain_db/rules(first_shot/strong_cut/every_n_cuts/last_shot)。 - subtitle:enabled/font/font_size/bold/color/outline/outline_color/shadow/margin_v/alignment/max_chars_per_line。 - ducking:enabled/threshold 是线性值(0.02≈-34dBFS,设太高完全不触发)/ratio/attack_ms/release_ms/level_sc/makeup_db。 - safe_area:✅ subtitle_bottom_px=400 已生效(与 subtitle.margin_v 联动);⛔ top_px/bottom_px/side_px 预留未接(画面模糊背景居中,暂不需按安全区裁切)。 - rhythm:✅ min_shot_frames=4 已生效;⛔ max_shot_s/hardcut_ratio/hook_window_s 预留未接(切镜密度现由 plan.py --shot-len 控制)。 - qc:duration_s(硬限兜底)/resolution/fps/loudness_lufs/tolerance/true_peak_dbtp/max_black_s/max_silence_s/first_frame_must_not_be_flat。 - platforms:douyin/wechat_channel/xiaohongshu/bilibili/offline_screen,各含 name/ratio/resolution/duration_s/bitrate。 - sfx_types:分类标签列表(whoosh/impact/riser/click/transition)。

6.2 profiles/_template.yaml(客户档案,引擎品牌无关)

6.3 新增客户步骤

  1. 复制 _template.yaml → profiles/<id>.yaml 填内容。
  2. 建 library/<id>/{video,image,audio/bgm},按 naming.template 命名素材。
  3. (可选)跑 python scripts/scan.py --client <id> 建索引。
  4. python scripts/caption.py 自动用品牌档案写台词;autocut.py 自动出片。

7. 关键技术决策与坑位清单(重做时最易再踩)

# 坑 正确做法
1 SRT 直接烧字幕 必须生成 ASS 并显式 PlayResX/PlayResY=成片分辨率
2 字幕/音效时间用 timeline[].at 必须减 window.start 转成片轴
3 混音用 -shortest 用 -t total 锁死时长
4 loudnorm 测量不带上 prechain 测量链 = 混音前置链(highpass+prechain)一致
5 配音流直接喂 sidechain + amix 先 asplit=2,sidechain 用 vo、amix 用 vo2
6 SFX 输出单声道 必须双声道,否则成片音轨降 1ch
7 切镜密度只看强弱拍 按 target_shot_s/beat_dur 算每镜占几拍
8 节拍表按 BGM 名无脑复用 校验 size+mtime+segment 指纹,换文件必重算
9 字幕滤镜写绝对路径带盘符 cwd 切到 ASS 目录 + 只写文件名;--out 用绝对路径
10 短 BGM 截短片 -stream_loop -1 -t total 循环铺满
11 逐镜与终稿编码参数不一致 用同一套 master 段 crf/preset
12 safe_area.top/side 与 rhythm.max_shot_s 等 当前预留未接,别花时间接(但保留字段)
13 brand.logo/intro/outro 尚未接入 render,仅 slogan/selling_points 生效
14 PY 路径硬编码 通过 AUTOCUT_PY 环境变量注入,避免本机路径写死
15 队列 onFinish 不清 current 会永久卡住后续任务(见 queue.js 注释)

8. 调度层(workbench/,零依赖 Node)

文件 职责
server.js http 服务(PORT 3091,HOST 0.0.0.0),静态 UI + /api/* 路由 + SSE
lib/runner.js Job 类:spawn autocut.py --progress-json,解析 @PROGRESS@ JSON 行,订阅推送;cancel() 用 taskkill /PID /T /F 杀进程树(Windows ffmpeg 残留)
lib/queue.js 串行队列(ffmpeg 吃满 CPU,并行无益);jobs.json 原子写(tmp+rename)持久化;重启恢复 running/pending→interrupted
lib/sysinfo.js 读本地:磁盘(PowerShell Win32_LogicalDisk)/metrics/alerts/assets/options/lanIp,不经服务器
index.html 前端 UI(触发层):提交渲染、看队列、SSE 进度、预览成片、拉远程任务、推送平台

核心路由: - POST /api/render {client,bgm,target,seed,density,scan,pick,voice} → 入队 - POST /api/batch {count,seed,...} → 批量多 seed 出片 - POST /api/pull-tasks → 从业务平台 /api/tasks + /task/:id 拉远程任务落地 work/tasks/<id>.json,转成队列任务 - GET /api/jobs GET /api/jobs/:id/stream(SSE)POST /api/jobs/:id/cancel POST /api/jobs/:id/retry DELETE /api/jobs/:id - GET /api/outputs POST /api/outputs/push → 调 scripts/sync-to-media.sh - GET /api/system/{disk,metrics,alerts,assets,info}

进度协议:autocut.py 在 --progress-json 时打印 @PROGRESS@<json>,含 stage/done/total/pct;runner.js 解析后推 SSE(STAGE_LABEL:scan/beats/plan/render/qc/done)。

取消:Windows 必须 taskkill /T /F 杀整棵进程树,否则 ffmpeg 残留占端口/CPU。


9. 远程同步(sync-to-media.sh)


10. 部署与运行恢复清单

必备份(不进 git,单独保管): 1. configs/remote.yaml(rclone 目标) 2. bin/rclone.conf(同步凭据) 3. .env(DeepSeek key) 4. 客户素材 library/<client>/ 与 library/_shared/(体积大,用 rclone 或外置盘) 5. 各 profiles/<client>.yaml

恢复步骤: 1. 拉仓库(含 scripts/configs/workbench/templates)。 2. 建 Python venv:pip install librosa numpy Pillow pyyaml;确认 ffmpeg/ffprobe 在 PATH。 3. 还原上述 4 类敏感/数据文件到对应位置。 4. 设 AUTOCUT_PY 指向 venv 的 python.exe(或改 runner.js 常量)。 5. python scripts/make_sfx.py 生成音效库(若 _shared/audio/sfx 未备份)。 6. 跑 python scripts/scan.py --client <id> 重建索引(或 rclone 拉回 library/<id>/index)。 7. 启动:node workbench/server.js → http://localhost:3091(手机同 WiFi 用 lanIp)。

最小冒烟测试:python scripts/autocut.py --client moen --bgm library/_shared/audio/bgm/bgm_calm_60s.wav --target 15 --mock-caption → 应产出 output/moen_<日期>_master.mp4 且 qc 9/9。


11. 当前边界与已知未接模块(重做时可取舍)


12. 重新实现里程碑建议(顺序)

  1. 脚手架:_common.py(路径/配置/ffprobe/命名校验)+ configs/platforms.yaml + profiles/_template.yaml。
  2. 建库:scan.py(索引 + 缩略图)。
  3. 节拍:beats.py(librosa + 缓存指纹)。
  4. 编排:plan.py(span 算法 + 轮转分配 + 白名单),先出 edl.json 人工核对。
  5. 渲染:render.py(逐镜 + concat + 音频链路,按 §5⑥ 坑位),先不出字幕验证母版。
  6. 字幕/音效:subtitle.py(ASS)+ sfx.py(numpy 时间轴),接 §7 坑位。
  7. 质检/指标:qc.py + metrics.py。
  8. AI 字幕:llm.py + caption.py(DeepSeek 接入,降级策略)。
  9. 派生:derive.py(多平台分组复用)。
  10. 调度层:workbench/(server/runner/queue/sysinfo + index.html + SSE + cancel)。
  11. 同步:sync-to-media.sh + remote.sh(rclone)。
  12. 冒烟 + 客户档案:按 §10 跑通 moen,再扩客户。

本文依据已运行版本源码整理,覆盖引擎层(scripts/ 12 个模块)、配置层(platforms.yaml / profiles)、调度层(workbench/ 5 个文件)与同步层。重做时严格照 §5 算法与 §7 坑位,可保证产物与质量门禁兼容。

本文为公开技术资料,依据已运行版本源码整理。重做时严格照文档算法与坑位清单,可保证产物与质量门禁兼容。