首次提交

This commit is contained in:
2026-09-28 16:09:31 +08:00
commit 4a6839319a
71 changed files with 12144 additions and 0 deletions

150
tools/video_bench.py Normal file
View File

@@ -0,0 +1,150 @@
# -*- coding: utf-8 -*-
"""
下载一个视频 → 抽帧 → 用这些【真实帧】对 OCR 端点做压测。
比随机生成的图更贴近线上负载(真实字幕内容,OCR 干的活和实际一致)。
用法(项目根目录运行):
python temp/video_bench.py <视频...> --url http://IP:18081 [选项]
<视频...> 一个或多个,cos://bucket/path/x.mp4 或本地 ./video/x.mp4
多个视频时各抽一部分帧合并成图池(max-frames 在视频间均分)
也可用 --list 文件 从文件读视频列表(每行一个)
选项:
--url URL OCR 端点(可多次,soak 只用第一个)
--concurrency N 并发(默认 100;单台拐点约 80)
--mode soak|sweep soak=持续满速(默认);sweep=自动加压找极限
--fps F 抽帧频率(默认 1,压测用不需要 3fps,省时省内存)
--max-frames N 最多用多少帧做图池(默认 2000)
--interval N soak 打印间隔秒(默认 5)
--ffmpeg PATH ffmpeg 路径(默认 ./bin/ffmpeg)
--coscli PATH coscli 路径(默认 ./coscli-linux-amd64)
--cos-config PATH coscli 配置(默认 .cos.yaml)
--keep 跑完不删临时视频/帧
"""
import argparse
import glob
import os
import shutil
import subprocess
import sys
import tempfile
# 复用同目录 bench 脚本的压测逻辑
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
import bench_ocr_endpoints as bench # noqa: E402
def download_video(src, coscli, cos_config, dst):
"""cos:// 路径用 coscli 下载到 dst;本地路径直接返回。"""
if not src.startswith("cos://"):
if not os.path.exists(src):
print(f"本地视频不存在: {src}")
return None
return src
print(f"下载视频: {src}")
r = subprocess.run([coscli, "-c", cos_config, "cp", src, dst],
capture_output=True, text=True)
if r.returncode != 0 or not os.path.exists(dst):
print(f"下载失败: {r.stderr.strip()[:300]}")
return None
print(f" → {dst} ({os.path.getsize(dst)/1024/1024:.0f} MB)")
return dst
def extract_frames(ffmpeg, video, frame_dir, fps):
"""用 ffmpeg 抽帧到 frame_dir,返回帧文件列表。"""
os.makedirs(frame_dir, exist_ok=True)
print(f"抽帧 (fps={fps}) ...")
cmd = [ffmpeg, "-i", video, "-vf", f"fps={fps}", "-q:v", "2",
os.path.join(frame_dir, "f_%06d.jpg"), "-loglevel", "error", "-y"]
r = subprocess.run(cmd, capture_output=True, text=True)
if r.returncode != 0:
print(f"抽帧失败: {r.stderr.strip()[:400]}")
return []
frames = sorted(glob.glob(os.path.join(frame_dir, "*.jpg")))
print(f" 抽出 {len(frames)} 帧")
return frames
def main():
ap = argparse.ArgumentParser()
ap.add_argument("videos", nargs="*", default=[], help="一个或多个视频(cos://... 或本地路径)")
ap.add_argument("--list", default=None, help="从文件读视频列表(每行一个,可代替直接传)")
ap.add_argument("--url", action="append", default=None, help="OCR 端点(可多次)")
ap.add_argument("--concurrency", type=int, default=100)
ap.add_argument("--mode", choices=["soak", "sweep"], default="soak")
ap.add_argument("--fps", type=float, default=1.0)
ap.add_argument("--max-frames", type=int, default=2000)
ap.add_argument("--interval", type=int, default=5)
ap.add_argument("--ffmpeg", default="./bin/ffmpeg")
ap.add_argument("--coscli", default="./coscli-linux-amd64")
ap.add_argument("--cos-config", default=".cos.yaml")
ap.add_argument("--keep", action="store_true")
args = ap.parse_args()
endpoints = bench.get_endpoints(args.url)
if not endpoints:
print("没有 OCR 端点,用 --url 指定")
return
# 收集视频列表:命令行参数 + --list 文件
videos = list(args.videos)
if args.list and os.path.exists(args.list):
with open(args.list, encoding="utf-8") as f:
videos += [ln.strip() for ln in f if ln.strip()]
if not videos:
print("没有视频,传一个或多个视频路径,或用 --list 文件")
return
# 每个视频均分帧额度,避免一个长视频占满图池
per_video_cap = max(1, args.max_frames // len(videos))
work = tempfile.mkdtemp(prefix="video_bench_")
try:
all_frames = []
for i, src in enumerate(videos):
print(f"\n[{i+1}/{len(videos)}] {src}")
vlocal = os.path.join(work, f"video_{i}.mp4")
fdir = os.path.join(work, f"frames_{i}")
v = download_video(src, args.coscli, args.cos_config, vlocal)
if not v:
continue
fr = extract_frames(args.ffmpeg, v, fdir, args.fps)
if len(fr) > per_video_cap:
fr = fr[:per_video_cap]
print(f" 取前 {per_video_cap} 帧")
all_frames += fr
# 处理完即删该视频,省空间
if v == vlocal:
try:
os.remove(vlocal)
except OSError:
pass
if not all_frames:
print("没抽到任何帧")
return
# 帧 → base64 图池
print(f"\n加载 {len(all_frames)} 帧为图池 ...")
pool = [bench.img2base64(f) for f in all_frames]
print("=" * 60)
print(f"图池: {len(pool)} 张真实帧(来自 {len(videos)} 个视频) | "
f"端点: {endpoints[0]} | 并发: {args.concurrency}")
print("=" * 60)
# 4) 压测
if args.mode == "sweep":
bench.sweep_endpoint(endpoints[0], pool[0], 10, 320, 6)
else:
bench.soak_endpoint(endpoints[0], pool, args.concurrency, args.interval)
finally:
if args.keep:
print(f"\n临时文件保留在: {work}")
else:
shutil.rmtree(work, ignore_errors=True)
if __name__ == "__main__":
main()