24式简化太极拳的完整版视频,配上晨练随拍的镜头,这类内容在短视频平台上非常常见:一个机位、一个人、一套动作从头打到尾,背向演练方便跟着学,正面机位则更适合看清手型步法。但真要把“一条视频”变成“一套能反复用的教学素材库”,难点就不在拳架上了,而在工程上——怎么批量转码、怎么自动生成字幕、怎么把24式拆成独立片段、怎么让背向和正面两个机位在时间轴上对齐、怎么用接口把整条流水线串起来跑批量任务。
这篇文章不绑定某一个具体的开源项目,而是围绕“一套太极拳教学视频的本地化处理”这个真实需求,给出一套可落地的工具链方案:FFmpeg 负责转码切片,Whisper 负责中文语音转写和字幕,MediaPipe 负责姿态关键点提取,FastAPI 负责把上面这些能力包装成接口,再用一个简单的任务队列跑批量。所有参数都不写死,实际数值以你本机测试为准。
如果你正在做武术、健身、舞蹈、体操这类动作教学内容的整理,或者手里有一堆“晨练随拍”素材想做成结构化的动作库,这篇文章可以直接照着搭。如果你只是想找一个现成的“一键生成教学视频”的软件,那这里没有——本文给的是可拆可改的工程流程,好处是每一环都能单独换、单独调、单独验证。
下面按“先看能力边界,再准备环境,然后一步步装、一步步测,最后接接口跑批量”的顺序展开。每一个功能点都会给出测试目的、输入、操作步骤、预期结果和失败排查,方便你边看边验证。
1. 核心能力速览
先把这套方案能干什么、门槛在哪说清楚。表格里的参数凡是需要按本机实测才能确定的,一律标注为“以实际测试为准”,不做臆测。
| 能力项 | 说明 |
|---|---|
| 方案类型 | 本地部署的视频处理 + 语音转写 + 姿态分析工具链 |
| 核心组件 | FFmpeg(转码/切片/抽帧)、Whisper(中文语音转写)、MediaPipe(人体姿态关键点)、FastAPI(接口服务) |
| 主要功能 | 视频批量转码、按动作切分片段、生成中文字幕、提取骨骼关键点、批量导出素材 |
| 硬件门槛 | FFmpeg 与 MediaPipe 可纯 CPU 运行;Whisper 使用 GPU 加速时显存需求随模型规模变化,需按实际模型版本测试 |
| 支持平台 | Windows / Linux / macOS 均可,具体依赖版本以官方文档为准 |
| 启动方式 | 命令行启动 + 可选 HTTP 接口服务 |
| 是否支持 API | 支持,可在本地自建 HTTP 接口封装上述能力 |
| 是否支持批量任务 | 支持,通过目录约定 + 任务队列实现 |
| 适合场景 | 武术教学素材整理、动作库建设、字幕批量生成、姿态对比分析 |
| 不适合场景 | 需要实时低延迟推理的直播场景、需要商用级精度的专业运动生物力学分析 |
这张表的核心信息其实就一句:这套东西的硬件门槛不高,CPU 也能跑通大部分环节,真正的成本在“把流程调对”和“把批量任务的稳定性做好”上。
从材料看,这条视频的内容形态是“背向演练 + 完整版 + 晨练随拍”,意味着画面里很可能有户外光线变化、背景干扰、机位轻微晃动这几个特征。这几点会直接影响后面姿态估计的稳定性,也会影响字幕对齐的准确度——不是工具不行,而是素材本身不够“干净”。所以后面的测试环节里,我会专门加一项“素材质量预检”。
另外要提前说清楚:这套流程里没有一步是“自动理解拳架对错”的。姿态关键点只能告诉你“手腕在这里、肘在这里、膝盖角度是多少”,它不会告诉你“这一式该不该沉肩坠肘”。那属于专业判断,得由人来。工具能做的,是把重复劳动压缩掉,把可量化的部分量化出来。
2. 适用场景与使用边界
2.1 这套方案真正适合谁
第一类是做内容归档的人。手里攒了几十上百条晨练随拍的视频,文件名乱七八糟,时长不一,分辨率不一,想整理成一个能按“式”检索的素材库。这套流程能把转码、切片、字幕、封面图这几件重复劳动全部批量化。
第二类是做教学产品的人。需要把 24 式拆成 24 个独立片段,每段配字幕、配封面、配关键帧图示,然后打包给学员。人工切 24 段不是不行,但换成 200 条视频就是灾难。
第三类是做动作分析对比的人。比如同一个动作,背向演练和正面示范各拍一条,想看看两者在时间轴上的关节角度差异。MediaPipe 输出的关键点序列可以直接拿去做曲线对比。
2.2 明确不适合的场景
实时场景不合适。Whisper 转写是离线批处理的设计,MediaPipe 虽然能跑视频流,但本文给的是“先落盘再处理”的流程,端到端延迟远高于直播需求。
高精度生物力学分析不合适。消费级姿态估计模型在户外逆光、宽松衣物、快速转身的情况下,关键点抖动是常态。拿它做康复评估或者运动损伤分析,风险太高。
全自动内容审核不合适。工具可以帮你把人脸、人体区域框出来,但“这段素材能不能公开发布”是法律和授权问题,不是算法问题。
2.3 使用边界与合规提醒
这一节必须单独强调,因为动作教学视频天然涉及两类权益。
第一类是肖像权。晨练随拍很容易拍到路人、同伴、教练。任何素材在公开发布或商用之前,必须确认画面中每一个可识别的人都已获得明确授权。批量处理流程里建议加一道“人脸区域检测 + 人工复核”的卡点,不要图省事直接跳过。
第二类是著作权。太极拳套路本身属于传统体育项目,但具体的教学视频、配乐、字幕文案、讲解内容都是有作者的。转载、二次剪辑、加字幕再发布,都要先确认授权范围。配乐尤其容易被忽略,很多平台的自动识别系统对背景音乐的敏感度远高于画面。
第三类是数据处理边界。如果你把视频上传到第三方服务做转写,等于把素材交出去了。本文坚持本地部署的原因之一就是这个:素材不出本机,转写结果、关键点数据、切片文件全部落在自己的磁盘上,隐私风险可控。
第四类是输出标注。用 AI 辅助生成的字幕、切分点、关键帧,建议在成品里保留“机器辅助生成,已人工校对”的说明。这不是形式主义,是让下游使用者知道哪些部分是可靠的、哪些部分需要复核。
3. 环境准备与前置条件
3.1 通用检查清单
在装任何东西之前,先把这几项确认掉,能省掉后面一大半的排查时间。
- 操作系统:Windows 10/11、主流 Linux 发行版、macOS 均可。Windows 上建议把工作目录放在非系统盘,避免路径过长和权限问题。
- Python:建议 3.9 以上版本,用虚拟环境隔离依赖,不要往系统 Python 里直接装。
- 磁盘空间:1080p 视频批量处理非常吃空间。一条 10 分钟的 1080p 素材,原始文件、转码副本、切片、抽帧图片加起来,占用可能是原始文件的 3 到 5 倍。预留空间按“素材总大小 × 5”估算比较稳妥。
- 显卡驱动:如果用 GPU 加速 Whisper,先确认驱动和 CUDA 版本匹配。驱动版本和 CUDA 版本不匹配是最常见的“装完了跑不起来”原因。
- 端口占用:接口服务默认端口要提前确认没被占用,冲突了直接换端口,不要硬刚。
- 编码器支持:FFmpeg 的硬件编码器(如 NVENC、QSV、VideoToolbox)需要对应硬件和编译选项支持,不确定就先用软件编码跑通流程。
3.2 依赖安装
下面是通用安装命令,具体版本号请按官方文档和你本机环境调整,不要照抄版本号。
# 创建并激活虚拟环境 python -m venv venv # Windows venv\Scripts\activate # Linux / macOS source venv/bin/activate # 安装核心依赖(版本请按官方文档确认,此处不锁定具体版本) pip install fastapi uvicorn python-multipart pip install openai-whisper pip install mediapipe pip install opencv-pythonFFmpeg 不走 pip,需要单独安装。Windows 建议下载官方构建包后把bin目录加入 PATH,Linux 用包管理器安装即可。
# Linux 示例 sudo apt update sudo apt install -y ffmpeg # 验证安装 ffmpeg -version ffprobe -version安装完成后立刻验证一遍,别等到跑批量任务的时候才发现 FFmpeg 根本没进 PATH。ffmpeg -version能正常输出版本信息,才算这一步过了。
3.3 目录结构约定
批量任务能不能跑顺,八成取决于目录结构定得好不好。建议在动手写代码之前就把目录定死,后面所有脚本都按这个约定走。
taiji_workspace/ ├── inputs/ # 原始素材,只读,不要在这里改文件 │ ├── 2024_spring_01.mp4 │ └── 2024_spring_02.mp4 ├── work/ # 中间产物:转码副本、音频、抽帧 │ ├── normalized/ │ ├── audio/ │ └── frames/ ├── outputs/ # 最终产物:切片、字幕、封面 │ ├── clips/ │ ├── subtitles/ │ └── covers/ ├── models/ # 本地模型文件目录 ├── logs/ # 每个任务的运行日志 └── config/ └── pipeline.json # 流水线配置关键原则就三条:原始素材目录只读、中间产物和最终产物分开、每个任务必须有独立日志。第三条尤其重要,批量任务卡住的时候,没有日志你只能靠猜。
4. 安装部署与启动方式
4.1 第一步:批量转码统一规格
不同手机、不同时间拍的素材,分辨率、帧率、编码格式大概率不一致。统一规格是后面所有步骤的前提,否则切片时间点对不上、抽帧数量对不上、姿态估计的输入尺寸也对不上。
#!/usr/bin/env bash # 批量转码脚本示例:把 inputs 目录下所有视频统一为 1080p / 30fps / H.264 # 实际参数请按你的素材和硬件调整 INPUT_DIR="./inputs" OUTPUT_DIR="./work/normalized" mkdir -p "$OUTPUT_DIR" for f in "$INPUT_DIR"/*.mp4; do name=$(basename "$f" .mp4) ffmpeg -y -i "$f" \ -vf "scale=1920:-2,fps=30" \ -c:v libx264 -preset medium -crf 20 \ -c:a aac -b:a 128k \ -movflags +faststart \ "$OUTPUT_DIR/${name}_norm.mp4" done这段脚本里几个参数值得说明。scale=1920:-2里的-2保证高度是偶数,H.264 对奇数高度不友好,容易直接报错。-crf 20是画质和体积的折中,教学视频不需要电影级画质,20 到 23 之间都够用。-movflags +faststart把索引信息挪到文件头,后面做流式预览会快很多。
如果你的机器有硬件编码器,把libx264换成对应编码器能快好几倍,但前提是先确认硬件支持。软件编码慢是慢,胜在稳定,第一次跑通流程建议先用软件编码。
4.2 第二步:音频抽取与语音转写
先把音频单独抽出来,再喂给 Whisper。直接让 Whisper 读视频也能跑,但它内部还是要解一遍音频流,不如提前抽好,后面复用方便。
# 抽取单声道 16k 音频,Whisper 对采样率不敏感,但统一规格能减少意外 ffmpeg -y -i ./work/normalized/2024_spring_01_norm.mp4 \ -vn -ac 1 -ar 16000 \ ./work/audio/2024_spring_01.wav # 使用 Whisper 命令行转写,输出 srt 字幕 whisper ./work/audio/2024_spring_01.wav \ --model small \ --language zh \ --output_format srt \ --output_dir ./outputs/subtitles模型规模的选择是个权衡。模型越大,中文标点和专业词的识别通常越稳,但速度慢、资源占用高。太极拳教学里会出现“云手”“倒卷肱”“揽雀尾”这类词,通用模型大概率会写错,所以转写完成后必须有一道术语校正。最简单的做法是维护一个替换词表,跑完转写后做一次字符串匹配替换。
# 术语校正示例:把常见误识别替换为正确写法 TERM_MAP = { "野马分宗": "野马分鬃", "白鹤亮翅": "白鹤亮翅", "倒卷宏": "倒卷肱", "揽雀尾": "揽雀尾", "云手": "云手", } def fix_terms(text: str) -> str: for wrong, right in TERM_MAP.items(): text = text.replace(wrong, right) return text if __name__ == "__main__": sample = "下面这一式是野马分宗,注意重心转换" print(fix_terms(sample))这个思路很土,但实用。术语表可以随着素材积累不断补充,跑得越多越准。不要指望一次调参就解决所有专业词问题,词表迭代才是正解。
4.3 第三步:启动姿态关键点提取
MediaPipe 的 Python 接口用起来很直接,核心就是按帧读、按帧推理、把关键点序列存下来。这一步建议先在单条短视频上验证,不要一上来就丢整段素材。
import cv2 import mediapipe as mp import json mp_pose = mp.solutions.pose def extract_keypoints(video_path: str, out_json: str): cap = cv2.VideoCapture(video_path) fps = cap.get(cv2.CAP_PROP_FPS) results_seq = [] with mp_pose.Pose( static_image_mode=False, model_complexity=1, enable_segmentation=False, min_detection_confidence=0.5, min_tracking_confidence=0.5, ) as pose: frame_idx = 0 while True: ok, frame = cap.read() if not ok: break rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) result = pose.process(rgb) if result.pose_landmarks: points = [ { "id": i, "x": round(lm.x, 4), "y": round(lm.y, 4), "z": round(lm.z, 4), "v": round(lm.visibility, 4), } for i, lm in enumerate(result.pose_landmarks.landmark) ] results_seq.append({"frame": frame_idx, "points": points}) frame_idx += 1 cap.release() with open(out_json, "w", encoding="utf-8") as f: json.dump({"fps": fps, "frames": results_seq}, f, ensure_ascii=False) return len(results_seq) if __name__ == "__main__": n = extract_keypoints("./work/normalized/2024_spring_01_norm.mp4", "./work/frames/kp_01.json") print(f"有效帧数: {n}")model_complexity这个参数直接影响速度和精度。设为 0 最快但精度最低,设为 2 最准但最慢。户外晨练随拍的画面里,人体占比通常不小,设为 1 是比较平衡的选择。min_detection_confidence调到 0.7 以上能减少误检,但也可能在逆光帧上直接丢检测,需要按素材实际表现调。
4.4 第四步:启动接口服务
前面三步都是命令行脚本,跑单条素材够用。要做批量,就得包一层接口。FastAPI 起服务很快,下面是一个最小可运行的服务骨架。
from fastapi import FastAPI, BackgroundTasks from pydantic import BaseModel import subprocess import uuid import os import json from datetime import datetime app = FastAPI(title="Taiji Media Pipeline") TASK_DIR = "./work/tasks" os.makedirs(TASK_DIR, exist_ok=True) class TranscodeRequest(BaseModel): input_path: str output_name: str | None = None class SubtitleRequest(BaseModel): audio_path: str model_size: str = "small" def _run(cmd: list[str], log_path: str): with open(log_path, "a", encoding="utf-8") as log: log.write(f"[{datetime.now()}] {' '.join(cmd)}\n") proc = subprocess.run(cmd, capture_output=True, text=True) log.write(proc.stdout) log.write(proc.stderr) return proc.returncode @app.post("/task/transcode") def create_transcode_task(req: TranscodeRequest, bg: BackgroundTasks): task_id = uuid.uuid4().hex[:12] log_path = os.path.join(TASK_DIR, f"{task_id}.log") out_name = req.output_name or f"{task_id}_norm.mp4" out_path = os.path.join("./work/normalized", out_name) cmd = [ "ffmpeg", "-y", "-i", req.input_path, "-vf", "scale=1920:-2,fps=30", "-c:v", "libx264", "-preset", "medium", "-crf", "20", "-c:a", "aac", "-b:a", "128k", "-movflags", "+faststart", out_path, ] bg.add_task(_run, cmd, log_path) return {"task_id": task_id, "status": "queued", "output": out_path} @app.get("/task/{task_id}/log") def get_task_log(task_id: str): log_path = os.path.join(TASK_DIR, f"{task_id}.log") if not os.path.exists(log_path): return {"error": "log not found"} with open(log_path, "r", encoding="utf-8") as f: return {"task_id": task_id, "log": f.read()[-4000:]}启动命令:
uvicorn app:app --host 127.0.0.1 --port 8000 --reload--host 127.0.0.1是刻意写成回环地址的。这类服务只在本机用,不要图方便绑到0.0.0.0,尤其在公共网络环境下。真需要跨机访问,也应该走内网加认证,不要直接暴露到公网。
5. 功能测试与效果验证
这一节按功能逐项测,每项都给判断标准。测试顺序建议从简单到复杂,别一上来就测最难的。
5.1 转码一致性测试
测试目的:确认批量转码后所有素材的规格统一。
输入:两到三条规格不同的素材,最好是横竖屏混合、帧率不同。
操作步骤:跑 4.1 的批量脚本,跑完后用ffprobe逐条查参数。
for f in ./work/normalized/*.mp4; do echo "=== $f ===" ffprobe -v error -select_streams v:0 \ -show_entries stream=width,height,r_frame_rate,codec_name \ -of default=noprint_wrappers=1 "$f" done预期结果:所有输出的width为 1920、height为 1080 或 1082 以下的偶数、r_frame_rate一致、codec_name为 h264。
判断成功的标准:参数全部对齐,且肉眼播放无明显卡顿、无音画不同步。
常见失败原因:源素材是可变帧率(VFR),转码后时间轴漂移。遇到这种情况,在 FFmpeg 命令里加-vsync cfr强制恒定帧率再跑一遍。
5.2 字幕转写与术语校正测试
测试目的:确认中文转写可用,术语能被校正到正确写法。
输入:一段 1 到 2 分钟的讲解音频,其中至少包含 3 个太极拳专业词。
操作步骤:先用 Whisper 跑一遍原始转写,看误识别情况;再套用 4.2 的术语表跑一遍替换;最后人工对照音频听一遍。
预期结果:日常用语的识别基本正确,专业词有错误但能被词表纠正大部分。
判断成功的标准:校正后字幕的错字率降到可以人工快速过一遍的程度,而不是要整段重写。
常见失败原因:音频里环境噪声大(户外晨练场景很常见),或者说话人离麦克风太远。这种情况下先做一次降噪预处理,或者直接改用外接麦克风重录。字幕这块,源头音质比模型大小重要得多。
5.3 动作切片定位测试
测试目的:验证按时间点切片的准确性。
24 式简化太极拳的完整演练,每一式之间通常有短暂的定势停顿。切片有两个思路:一是人工标注每式的起止时间码,写进配置文件;二是用姿态能量变化自动找停顿点。前者准确但费人工,后者省事但需要校对。
{ "video": "2024_spring_01_norm.mp4", "fps": 30, "segments": [ {"name": "01_起势", "start": 0.0, "end": 12.5}, {"name": "02_左右野马分鬃", "start": 12.5, "end": 31.0}, {"name": "03_白鹤亮翅", "start": 31.0, "end": 40.2} ] }操作步骤:先手工标 3 到 5 个片段做基准,用 FFmpeg 按时间码切出来,播放确认起止点是否准确。
# 按时间码切片,-ss 放在 -i 前面能更快定位 ffmpeg -y -ss 12.5 -to 31.0 -i ./work/normalized/2024_spring_01_norm.mp4 \ -c copy ./outputs/clips/02_左右野马分鬃.mp4预期结果:切片起止点和手工标注一致,画面没有黑帧、没有音频截断。
判断成功的标准:切出来的片段能独立播放,动作完整不残缺。
常见失败原因:-c copy是流复制,不重新编码,起止点会吸附到最近的关键帧上,导致实际切点偏差。如果对精度要求高,去掉-c copy改成重新编码,代价是速度慢很多。
5.4 姿态关键点稳定性测试
测试目的:确认在户外素材上,关键点不会频繁丢失或剧烈抖动。
输入:一段包含转身、下蹲、单腿支撑动作的素材。
操作步骤:跑 4.3 的脚本,输出 JSON 后统计有效帧占比和关键点跳变幅度。
import json with open("./work/frames/kp_01.json", "r", encoding="utf-8") as f: data = json.load(f) frames = data["frames"] total = len(frames) print(f"有效帧数: {total}, fps: {data['fps']}") # 统计手腕关键点(索引 15/16)的相邻帧位移 LEFT_WRIST = 15 jumps = [] for i in range(1, total): a = frames[i - 1]["points"][LEFT_WRIST] b = frames[i]["points"][LEFT_WRIST] dist = ((a["x"] - b["x"]) ** 2 + (a["y"] - b["y"]) ** 2) ** 0.5 jumps.append(dist) if jumps: avg = sum(jumps) / len(jumps) big = sum(1 for j in jumps if j > 0.1) print(f"平均帧间位移: {avg:.4f}, 大跳变帧数: {big}")预期结果:有效帧占比在大多数正常光线下应该比较高,大跳变帧数占比低。
判断成功的标准:关键点序列能画出一条连续曲线,没有大量突刺。
常见失败原因:宽松衣物导致手臂轮廓模糊、逆光导致人体与背景对比度不足、快速转身时人体出画。这三种情况都不是调参能彻底解决的,只能从拍摄端改善——穿贴身一点的深色衣物、避免正对强光源、机位稍微拉远保证全身始终在画面内。
5.5 批量任务串联测试
测试目的:验证从转码到字幕到切片的完整链路能无人值守跑完。
操作步骤:把 10 条素材放进inputs,写一个调度脚本按顺序跑完四个阶段,每个阶段检查退出码。
判断成功的标准:全部任务跑完,outputs目录里每条素材都有对应的切片、字幕、关键点文件,logs目录里每条都有完整日志。
常见失败原因:某一条素材编码异常导致 FFmpeg 报错退出,脚本没有做错误捕获,整个批量中断。这个坑非常常见,一定要在循环里对每条素材单独 try/except,单条失败记日志继续跑,不要整体崩。
6. 接口 API 与批量任务
6.1 接口设计原则
批量处理场景下的接口,和给前端页面调的接口,设计思路不一样。前者更关心三件事:任务能不能异步跑、状态能不能查、失败了能不能重试。所以接口至少要提供“提交任务”“查状态”“查日志”三个能力,而不是同步等结果返回。
同步接口在短视频上能跑,在 10 分钟素材上就会超时。HTTP 请求挂在那儿等几分钟,中间任何网络抖动都会前功尽弃。异步加轮询是最稳的模式。
6.2 调用示例
服务起来之后,用 curl 提交一个任务:
curl -X POST "http://127.0.0.1:8000/task/transcode" \ -H "Content-Type: application/json" \ -d '{"input_path": "./inputs/2024_spring_01.mp4", "output_name": "01_norm.mp4"}'返回里会带task_id,拿这个 id 轮询日志:
curl "http://127.0.0.1:8000/task/<task_id>/log"Python 侧的批量调用封装:
import requests import time import glob import os BASE = "http://127.0.0.1:8000" INPUT_DIR = "./inputs" def submit(path: str) -> str: resp = requests.post( f"{BASE}/task/transcode", json={"input_path": path, "output_name": os.path.basename(path)}, timeout=30, ) resp.raise_for_status() return resp.json()["task_id"] def wait_done(task_id: str, timeout: int = 1800, interval: int = 10) -> bool: start = time.time() while time.time() - start < timeout: try: r = requests.get(f"{BASE}/task/{task_id}/log", timeout=15) log = r.json().get("log", "") if "Output #0" in log or "muxing overhead" in log: return True except requests.RequestException: pass time.sleep(interval) return False if __name__ == "__main__": results = [] for f in sorted(glob.glob(os.path.join(INPUT_DIR, "*.mp4"))): try: tid = submit(f) ok = wait_done(tid) results.append((f, tid, ok)) print(f"{f} -> {tid} -> {'OK' if ok else 'TIMEOUT'}") except Exception as e: results.append((f, None, False)) print(f"{f} -> 提交失败: {e}") print(f"完成 {sum(1 for r in results if r[2])} / {len(results)}")6.3 批量任务的队列设计
文件系统队列在中小规模下够用,规模上去再考虑真正的消息队列。这里给一个最小可用的设计思路。
目录即队列:在work/tasks下建pending、running、done、failed四个子目录。提交任务就是往pending里写一个 JSON 描述文件,worker 进程定期扫描pending,取到任务后先mv到running,跑完mv到done或failed。
这个设计的好处是原子性由文件系统保证。mv在同一分区内是原子操作,不会出现两个 worker 抢同一个任务的情况。坏处是不支持优先级和复杂的依赖关系,但对“转码 + 转写 + 切片”这种线性流水线足够了。
失败重试建议做三层:第一层是任务内部重试,FFmpeg 遇到临时文件锁可以重试两三次;第二层是阶段级重试,转写失败但转码成功,重跑时跳过已完成的阶段;第三层是人工介入,把重试次数超限的任务统一挪到failed目录,人工看日志处理。三层都做完,批量任务的稳定性会有质的提升。
6.4 接口安全边界
本地服务也要讲边界。三条底线:绑定回环地址、加一个简单的 token 校验、限制上传文件的大小和类型。这三条不难做,但很多人图省事就跳过了。
from fastapi import Header, HTTPException API_TOKEN = "change-me-to-a-random-string" def verify_token(x_api_token: str = Header(default="")): if x_api_token != API_TOKEN: raise HTTPException(status_code=401, detail="invalid token")token 不要硬编码进代码库,从环境变量读。这个习惯在本地开发时看着麻烦,但一旦这个服务被搬到内网给同事用,硬编码的 token 就是个大坑。
7. 资源占用与性能观察
7.1 怎么观察资源占用
Windows 上任务管理器看 GPU 显存和 CPU 占用,Linux 上用nvidia-smi配合htop。更细的做法是在 Python 脚本里打点记录每阶段的耗时和峰值内存。
import time import tracemalloc from contextlib import contextmanager @contextmanager def profile(stage: str): tracemalloc.start() t0 = time.time() try: yield finally: current, peak = tracemalloc.get_traced_memory() tracemalloc.stop() print(f"[{stage}] 耗时 {time.time() - t0:.1f}s, 峰值内存 {peak / 1024 / 1024:.1f}MB")用这个包住每个阶段,跑几条素材就能拿到一份粗糙但真实的性能画像。
7.2 各阶段的性能特征
转码是整个流程里最耗 CPU 的一环,耗时和视频时长基本成正比,和分辨率、编码参数强相关。软件编码在普通消费级 CPU 上处理 1080p 素材,速度大概是实时的 1 到 3 倍,也就是 10 分钟素材要跑 3 到 10 分钟。开硬件编码能快一个数量级,但要注意画质损失和兼容性。
Whisper 转写的瓶颈在模型规模。模型越大越慢、占用越高,具体占用需以实际模型版本和推理参数测试为准。想压资源有三个方向:换小模型、把音频切成短段分批处理、用 GPU 加速。前两个牺牲的是准确率或者流程复杂度,第三个牺牲的是显存。
MediaPipe 姿态估计是纯 CPU 也能跑的,速度取决于输入分辨率。这里有个非常实用的优化点:不要拿原始 1080p 帧直接推理,先缩到 640 左右再喂进去,速度能快好几倍,而关键点精度损失很小。人体在画面里本来就占大头,降采样不会把人体降没。
切片这一步如果只做流复制,几乎不耗资源;一旦改成重新编码,就回到了转码的性能特征。
7.3 降低资源占用的实操手段
降低输入分辨率是第一优先级。抽帧时按比例缩放,姿态估计用缩放后的帧,UI 上展示原图。这一条能省掉一半以上的计算量。
减少不必要的中间文件。转码副本和抽帧图片都占磁盘,跑完一个阶段就清理上一个阶段的临时文件。但注意别把中间产物全清了,出了问题要能回溯,建议保留最近一次任务的中间产物。
控制并发数。批量任务不要开太多并行,CPU 和显存都是有限资源,开 8 个转码进程不如开 2 个跑得稳。并发数的合理值大概是 CPU 核心数的一半,GPU 任务建议串行。
避免进程残留。FFmpeg 异常退出后子进程可能还挂着,批量跑久了会积累一堆僵尸进程。在脚本里对 subprocess 加超时,超时直接 kill。
7.4 分辨率、帧率、批量的影响
这三个参数是性能杠杆,动一个就动全身。分辨率翻倍,计算量大概翻四倍;帧率从 60 降到 30,姿态估计的计算量直接减半;批量数翻倍,总耗时就翻倍,但单位耗时会略降,因为启动开销被摊薄了。
实际调的时候建议按这个顺序:先把帧率降到够用(动作教学 25 到 30 帧基本够),再把分析分辨率降下来,最后才考虑减少批量数。前两个不影响业务,第三个影响吞吐。
8. 常见问题与排查方法
| 问题现象 | 可能原因 | 排查方式 | 解决方案 |
|---|---|---|---|
ffmpeg: command not found | 未安装或未加入 PATH | 终端执行ffmpeg -version | 安装后把 bin 目录加入 PATH,重启终端 |
转码报height not divisible by 2 | 缩放后高度为奇数 | 看 FFmpeg 错误输出 | 缩放参数用scale=1920:-2,用 -2 而不是 -1 |
| 转码后音画不同步 | 源素材可变帧率 | ffprobe查看r_frame_rate和avg_frame_rate是否一致 | 加-vsync cfr强制恒定帧率重跑 |
| Whisper 装完 import 报错 | 依赖版本冲突 | 看完整 traceback | 用干净虚拟环境重装,不要和系统包混用 |
| Whisper 中文标点全丢 | 语言参数未指定或模型太小 | 检查--language zh | 显式指定中文,换更大的模型测试 |
| 专业词大量识别错误 | 通用模型领域适配不足 | 抽查转写字幕 | 维护术语替换表,做后处理校正 |
| MediaPipe 关键点频繁丢失 | 逆光、宽松衣物、人体出画 | 看有效帧占比 | 改善拍摄条件,调低检测置信度阈值 |
| 关键点剧烈抖动 | 快速动作 + 低分辨率 | 统计帧间位移分布 | 提高分析分辨率,加时间平滑滤波 |
| 接口请求超时 | 同步接口处理长视频 | 看服务端是否还在跑 | 改异步任务 + 轮询日志 |
| 批量任务中途中断 | 单条素材异常未捕获 | 查 logs 目录最后一个日志 | 循环内加 try/except,单条失败继续 |
| 端口被占用 | 服务已在运行或端口冲突 | netstat -ano查端口 | 换端口或结束占用进程 |
| 磁盘快速耗尽 | 中间产物未清理 | du -sh看目录大小 | 分阶段清理,保留最近一次任务产物 |
| 显存不足 OOM | 模型太大或批量并发过高 | 看 GPU 显存曲线 | 换小模型,批量改串行 |
| 切片起止点偏移 | -c copy吸附到关键帧 | 对比切点和手工标注 | 去掉-c copy重新编码,或接受偏差 |
这张表里的每一条都是从实际操作里最容易撞上的。真正排查的时候有个通用心法:先看日志,再看参数,最后才怀疑工具本身。绝大多数“跑不起来”都是参数不对或者环境没配对,不是软件有 bug。
另外补一个容易忽略的点:路径里有中文或空格,在 Windows 上会让一批命令行工具直接报错。工作目录尽量用纯英文加下划线,这条能省掉很多莫名其妙的报错。
9. 最佳实践与使用建议
9.1 先跑通最小闭环,再谈批量
第一次搭这套流程,不要直接上完整素材库。找一条 30 秒的短视频,走完“转码 → 抽音频 → 转写 → 关键点 → 切一片”的完整链路。链路通了,再换 5 分钟素材看耗时,最后才上批量。这个顺序能把问题定位在最小范围内。
9.2 配置外置,不要硬编码
路径、端口、模型大小、切片时间码、术语表,全部放进配置文件。代码里只读配置,不写死任何具体值。好处是换素材、换机器、换模型的时候不用改代码。
{ "input_dir": "./inputs", "work_dir": "./work", "output_dir": "./outputs", "video": { "target_width": 1920, "target_fps": 30, "crf": 20 }, "asr": { "model_size": "small", "language": "zh", "term_map_path": "./config/terms.json" }, "pose": { "model_complexity": 1, "min_detection_confidence": 0.5 }, "server": { "host": "127.0.0.1", "port": 8000 } }9.3 目录分层,产物可追溯
输入、中间产物、最终产物三层分开,每一层用统一的命名规则。建议在文件名里带日期和机位标识,比如2024spring_back_01_norm.mp4,back表示背向机位。背向和正向两个机位后期要做动作对齐时,这套命名规则能省掉大量人工匹配。
9.4 批量任务必须有日志和重试
每一条素材、每一个阶段、每一次重试,都要落到日志文件里。日志要包含时间戳、命令行、退出码、关键错误行。有了这三样,出问题的时候不用重现,直接看日志就能定位。
重试策略要区分错误类型。文件被占用、网络抖动这类瞬时错误可以重试;编码格式不支持、模型文件缺失这类确定性错误重试没用,直接进失败队列。不加区分地无脑重试,只会浪费时间。
9.5 合规永远是第一道闸门
再强调一次:涉及人像的素材,公开发布前必须拿到授权。批量流程里建议把“人脸区域检测”做成一个独立阶段,输出的检测结果供人工复核,而不是直接自动放行。配乐、字幕文案、讲解内容的授权同样要确认。工具能提升效率,但不能替人承担法律责任。
9.6 保留人工复核环节
字幕要人工过一遍,切片点要人工确认,关键点数据要抽样看一眼。AI 辅助的价值在于把 90% 的重复劳动干掉,剩下的 10% 判断必须由人来做。把人工复核设计成流程里的固定环节,而不是“有空再看”,成品质量会稳定很多。
10. 总结与下一步
回到最开始那个需求:一条背向演练的 24 式简化太极拳完整版视频,怎么变成一套可复用、可检索、可批量的教学素材库。答案不是某一个神器,而是几个成熟工具的组合——FFmpeg 管视频、Whisper 管语音、MediaPipe 管姿态、FastAPI 管调度,中间用目录约定和日志把整条链路串起来。
最值得先试的是转码和切片这两步,因为它们立刻能带来收益,而且几乎没有精度风险。第二步试字幕转写,配合术语表做后处理,实用价值很高。姿态关键点可以放到最后,它的用途更偏分析,不是每条素材都需要跑。
最容易踩的坑有三个。一是源素材规格不统一导致后续时间轴全乱,解决办法是转码阶段强制统一帧率。二是批量任务没有错误捕获,一条素材炸掉整个队列。三是路径和端口这类环境问题,看着低级但出现频率极高。
如果想继续往下扩,几个方向可以试试。第一是把关键点序列做成动作相似度对比,同一个动作两个机位的曲线画在一张图上,直观看出差异。第二是把切片结果接一个简单的检索页面,按“式名”搜索直接跳到对应片段。第三是把整条流水线容器化,换机器部署时不用重新配环境。第四是把术语表做成可增量维护的知识库,跑得越多越准。
这套流程的价值不在于技术多新,而在于它把一个纯手工的活变成了可重复执行的工程。建议先把最小闭环跑通,再按自己的素材特点慢慢加环节,不要一上来就追求全自动。工具是给人用的,流程是给人维护的,稳定比炫技重要。