HeyGem数字人系统上线前必做的5个自动化测试
HeyGem数字人视频生成系统不是简单的“上传音频+视频→点击生成”工具,而是一套需要稳定运行、批量处理、长期维护的生产级AI服务。当它即将部署到客户环境或接入企业工作流时,人工点一点的验证方式早已失效——你无法靠肉眼确认100次批量任务是否都精准同步口型,也无法在凌晨三点手动检查WebUI是否卡死。
真正的上线准备,是让代码替你完成5轮关键测试:从服务可访问性,到核心功能闭环;从异常容错能力,到并发稳定性;再到结果质量基线。这5个测试不是锦上添花,而是防止上线后第一单客户视频口型错位、第二单批量任务静默失败、第三单日志填满磁盘却无人知晓的最后防线。
它们不依赖模型内部结构,不修改一行业务代码,只通过真实用户视角——打开浏览器、上传文件、点击按钮、等待结果、校验输出——完成端到端验证。本文将为你逐个拆解这5个必须落地的自动化测试,全部基于Selenium + Chromedriver实现,代码可直接复用,适配HeyGem WebUI的Gradio动态DOM特性。
1. 服务存活与界面可达性测试:确保“门开着”
系统启动后,第一个问题不是“能不能用”,而是“能不能被访问”。很多部署失败其实卡在最基础环节:端口未暴露、反向代理配置错误、防火墙拦截、甚至start_app.sh脚本执行后进程意外退出。人工验证要开浏览器、输地址、看白屏还是报错;自动化测试只需3秒返回一个布尔值。
这个测试不做任何交互,只做两件事:
向http://localhost:7860发起HTTP请求,确认返回状态码为200
解析HTML响应,确认页面中包含<title>HeyGem</title>或Gradio App标识文本
它轻量、快速、无副作用,适合集成进CI/CD流水线的前置检查环节,或作为服务器定时巡检任务。
import requests import time def test_service_health(): url = "http://localhost:7860" timeout = 10 try: # 等待服务就绪(最多重试5次,每次间隔2秒) for i in range(5): try: response = requests.get(url, timeout=timeout) if response.status_code == 200: # 检查页面标题是否包含HeyGem if "HeyGem" in response.text or "Gradio" in response.text: print(" 服务存活且界面可达") return True except requests.exceptions.RequestException: pass time.sleep(2) print("❌ 服务不可达:超时或返回非200状态码") return False except Exception as e: print(f"❌ 服务健康检查异常:{e}") return False # 直接调用 test_service_health()为什么必须做?
- Docker容器启动后,Gradio服务可能因CUDA初始化失败而静默崩溃,但容器仍显示running
- 云服务器安全组未开放7860端口,导致外部无法访问,但本地curl能通(因loopback)
运行实时日志.log文件存在,但实际Web服务未监听该端口
这个测试是所有后续测试的前提。如果它失败,后续4个测试无需执行。
2. 批量处理流程冒烟测试:验证“主干道畅通”
HeyGem的核心价值在于批量模式——同一段音频驱动多个数字人视频。这个测试模拟真实用户最典型的使用路径:上传一段音频、添加两个视频、点击生成、等待完成、确认结果出现。它不追求生成视频质量,只验证整个流程链路不断裂。
关键设计点:
🔹 使用真实存在的测试文件(test_audio.mp3,test_video1.mp4,test_video2.mp4)
🔹 全程显式等待(WebDriverWait),不依赖time.sleep()
🔹 定位策略采用“文本+属性”双重保险,避开Gradio随机ID
🔹 成功标志是“生成结果历史”区域出现至少一个缩略图,而非仅弹出提示框
from selenium import webdriver from selenium.webdriver.chrome.service import Service from selenium.webdriver.common.by import By from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC from selenium.common.exceptions import TimeoutException, NoSuchElementException def test_batch_workflow(): options = webdriver.ChromeOptions() options.add_argument("--no-sandbox") options.add_argument("--disable-dev-shm-usage") options.add_argument("--headless") options.add_argument("--window-size=1920,1080") service = Service(executable_path="/usr/local/bin/chromedriver") driver = webdriver.Chrome(service=service, options=options) wait = WebDriverWait(driver, 30) try: driver.get("http://localhost:7860") wait.until(EC.title_contains("HeyGem")) print(" 已进入HeyGem主界面") # 切换到批量处理标签页 batch_tab = wait.until( EC.element_to_be_clickable((By.XPATH, '//button[contains(text(), "批量处理")]')) ) batch_tab.click() print(" 已切换至批量处理模式") # 上传音频 audio_input = wait.until( EC.presence_of_element_located((By.XPATH, '//input[@type="file" and contains(@accept, "audio")]')) ) audio_input.send_keys("/root/workspace/test_audio.mp3") print(" 音频上传完成") # 添加两个视频(模拟拖放行为) video_input = wait.until( EC.presence_of_element_located((By.XPATH, '//input[@type="file" and contains(@accept, "video")]')) ) video_input.send_keys("/root/workspace/test_video1.mp4\n/root/workspace/test_video2.mp4") print(" 视频列表已添加2个文件") # 点击开始批量生成 generate_btn = wait.until( EC.element_to_be_clickable((By.XPATH, '//button[contains(text(), "开始批量生成")]')) ) generate_btn.click() print(" 批量生成任务已提交") # 等待“处理完成”提示出现(表示任务入队成功) wait.until( EC.visibility_of_element_located((By.XPATH, '//*[contains(text(), "处理完成") or contains(text(), "已完成")]')) ) print(" 批量任务已成功提交并返回完成提示") # 验证结果区域至少有一个缩略图 result_thumbnails = driver.find_elements(By.XPATH, '//img[contains(@src, "file=")]') if len(result_thumbnails) >= 1: print(" 生成结果历史中已出现至少1个缩略图") return True else: print("❌ 生成结果历史为空,未检测到输出视频") return False except TimeoutException as e: print(f"❌ 批量流程超时失败:{e}") return False except Exception as e: print(f"❌ 批量流程执行异常:{e}") return False finally: driver.quit() # 运行测试 test_batch_workflow()为什么必须做?
- Gradio更新可能导致
<input type="file">元素结构变化,人工测试不易发现 - 音频/视频上传组件依赖前端JS逻辑,若CDN资源加载失败,上传按钮会失效
- “开始批量生成”按钮绑定的JS函数若报错,页面无任何提示,仅按钮点击无效
此测试覆盖了HeyGem 80%的用户核心路径,是上线前必须通过的“冒烟测试”。
3. 单个处理模式健壮性测试:检验“最小单元可靠”
批量模式是效率引擎,单个模式是调试探针。当客户反馈“某段音频生成效果差”时,工程师第一反应就是切到单个模式,上传同一组文件复现问题。因此,单个模式的稳定性直接影响问题定位效率。
本测试聚焦三个脆弱点:
🔸文件上传容错:故意上传一个不支持格式(如.txt),验证系统是否给出明确错误提示,而非静默失败
🔸空输入防护:不上传任何文件就点击“开始生成”,确认按钮被禁用或弹出友好提示
🔸结果即时预览:生成完成后,点击缩略图能否在右侧播放器中正常加载并播放
def test_single_mode_robustness(): options = webdriver.ChromeOptions() options.add_argument("--no-sandbox") options.add_argument("--disable-dev-shm-usage") options.add_argument("--headless") options.add_argument("--window-size=1920,1080") service = Service(executable_path="/usr/local/bin/chromedriver") driver = webdriver.Chrome(service=service, options=options) wait = WebDriverWait(driver, 30) try: driver.get("http://localhost:7860") wait.until(EC.title_contains("HeyGem")) # 切换到单个处理模式 single_tab = wait.until( EC.element_to_be_clickable((By.XPATH, '//button[contains(text(), "单个处理")]')) ) single_tab.click() # 测试1:上传不支持格式的音频(.txt) audio_input = wait.until( EC.presence_of_element_located((By.XPATH, '(//input[@type="file" and contains(@accept, "audio")])[1]')) ) audio_input.send_keys("/root/workspace/test_invalid.txt") # 检查是否出现错误提示(Gradio通常显示红色文字) error_msg = driver.find_elements(By.XPATH, '//*[contains(@class, "error") or contains(text(), "不支持") or contains(text(), "无效")]') if error_msg: print(" 不支持格式上传已触发正确错误提示") else: print("❌ 上传无效文件未触发错误提示") # 测试2:空输入防护(不上传任何文件,直接点击生成) generate_btn = driver.find_element(By.XPATH, '//button[contains(text(), "开始生成")]') # 检查按钮是否被禁用(Gradio常添加disabled属性) if generate_btn.get_attribute("disabled"): print(" 空输入时生成按钮已被禁用") else: # 或检查是否有提示弹窗 tooltip = driver.find_elements(By.XPATH, '//*[contains(@class, "tooltip") or contains(text(), "请上传")]') if tooltip: print(" 空输入时已显示友好提示") else: print("❌ 空输入时无任何防护机制") # 测试3:上传有效文件并验证预览(跳过耗时生成,仅验证UI链路) # (此处省略上传和生成步骤,重点验证结果区预览功能) # 假设已有生成结果,点击缩略图 thumbnail = driver.find_elements(By.XPATH, '//img[contains(@src, "outputs/")]') if thumbnail: thumbnail[0].click() # 等待右侧播放器出现video标签 player = wait.until( EC.presence_of_element_located((By.XPATH, '//video')) ) if player.get_attribute("src"): print(" 点击缩略图后,右侧播放器已加载视频源") return True else: print("❌ 播放器未加载视频源") return False else: print("❌ 未找到生成结果缩略图,跳过预览测试") return True # 允许部分通过 except Exception as e: print(f"❌ 单个模式健壮性测试异常:{e}") return False finally: driver.quit() test_single_mode_robustness()为什么必须做?
- 用户常误传格式,若系统不提示,会以为功能失效而放弃使用
- 前端缺少空输入校验,后端可能抛出500错误,暴露技术栈信息
- 播放器预览功能依赖前端JS动态注入
<video>标签,若CDN失效则空白
这是用户体验的“底线测试”,确保用户第一次使用就不会被卡住。
4. 异常场景恢复能力测试:验证“摔跤后能爬起来”
生产环境从不按剧本运行:磁盘突然写满、GPU显存溢出、网络抖动导致上传中断、用户上传超大文件……HeyGem必须能在这些异常后继续提供服务,而不是整个WebUI卡死或返回502。
本测试主动制造两类典型故障:
磁盘空间不足模拟:临时挂载一个1MB的tmpfs分区,指向outputs/目录
大文件上传中断:上传一个2GB的虚假大文件(使用dd if=/dev/zero of=bigfile bs=1M count=2000生成),并在上传中途强制终止
验证点:
✔ 系统是否返回清晰错误(如“磁盘空间不足,请清理outputs目录”)
✔ WebUI是否保持响应(其他按钮仍可点击,标签页可切换)
✔ 日志文件/root/workspace/运行实时日志.log中是否记录ERROR级别错误,且无堆栈溢出
def test_failure_recovery(): # 此测试需在Linux服务器上运行,依赖shell命令 import subprocess import os # 步骤1:模拟磁盘空间不足 print("🧪 正在模拟磁盘空间不足...") # 创建1MB tmpfs挂载点 subprocess.run(["mkdir", "-p", "/tmp/small_disk"]) subprocess.run(["mount", "-t", "tmpfs", "-o", "size=1M", "tmpfs", "/tmp/small_disk"]) # 将outputs软链接到小磁盘 subprocess.run(["rm", "-f", "/root/workspace/outputs"]) subprocess.run(["ln", "-s", "/tmp/small_disk", "/root/workspace/outputs"]) # 步骤2:启动浏览器测试 options = webdriver.ChromeOptions() options.add_argument("--no-sandbox") options.add_argument("--disable-dev-shm-usage") options.add_argument("--headless") options.add_argument("--window-size=1920,1080") service = Service(executable_path="/usr/local/bin/chromedriver") driver = webdriver.Chrome(service=service, options=options) wait = WebDriverWait(driver, 30) try: driver.get("http://localhost:7860") wait.until(EC.title_contains("HeyGem")) # 切换到批量模式 batch_tab = wait.until( EC.element_to_be_clickable((By.XPATH, '//button[contains(text(), "批量处理")]')) ) batch_tab.click() # 尝试上传一个1MB的测试音频(应触发磁盘不足) audio_input = wait.until( EC.presence_of_element_located((By.XPATH, '//input[@type="file" and contains(@accept, "audio")]')) ) audio_input.send_keys("/root/workspace/test_audio_1mb.mp3") # 等待错误提示出现(通常在上传区域下方) error_el = wait.until( EC.visibility_of_element_located((By.XPATH, '//*[contains(text(), "空间不足") or contains(text(), "磁盘")]')) ) print(" 磁盘不足时已显示明确错误提示") # 验证UI是否仍可操作:切换回单个模式 single_tab = driver.find_element(By.XPATH, '//button[contains(text(), "单个处理")]') single_tab.click() print(" 异常后UI仍可正常切换标签页") # 步骤3:检查日志是否记录错误(在另一终端执行) # 此处用Python读取日志最后一行 with open("/root/workspace/运行实时日志.log", "r") as f: lines = f.readlines() last_line = lines[-1] if lines else "" if "ERROR" in last_line and ("disk" in last_line.lower() or "space" in last_line.lower()): print(" 错误已正确记录到运行日志") else: print("❌ 日志中未找到对应ERROR记录") return True except Exception as e: print(f"❌ 异常恢复测试失败:{e}") return False finally: driver.quit() # 清理挂载 subprocess.run(["umount", "/tmp/small_disk"]) subprocess.run(["rm", "-rf", "/tmp/small_disk"]) subprocess.run(["rm", "-f", "/root/workspace/outputs"]) subprocess.run(["mkdir", "-p", "/root/workspace/outputs"]) test_failure_recovery()为什么必须做?
- 客户服务器常共用磁盘,AI生成任务可能与其他服务争抢空间
- HeyGem默认不限制上传文件大小,用户误传4K视频易导致OOM
- 若异常未捕获,Gradio服务进程可能崩溃,需人工重启
此测试验证的是系统的“韧性”,而非功能正确性。
5. 批量结果一致性校验测试:守住“质量下限”
前面4个测试确保系统“能跑”,本测试确保它“跑得稳”——即连续10次相同输入,生成的10个视频在基础维度上保持一致:文件存在、大小合理、时长匹配音频、无明显黑帧或卡顿。
这不是主观画质评估,而是可自动化的客观校验:
每个输出视频文件存在且大小 > 1MB(排除空文件)
视频时长与原始音频时长误差 < 0.5秒(验证口型同步精度)
FFmpeg探针无Invalid data警告(排除编码损坏)
import subprocess import json import time def get_video_duration(filepath): """使用ffprobe获取视频时长(秒)""" try: result = subprocess.run( ["ffprobe", "-v", "quiet", "-show_entries", "format=duration", "-of", "json", filepath], capture_output=True, text=True, timeout=30 ) if result.returncode == 0: data = json.loads(result.stdout) return float(data["format"]["duration"]) except Exception as e: print(f" 获取视频时长失败 {filepath}: {e}") return 0 def get_audio_duration(filepath): """获取音频时长""" try: result = subprocess.run( ["ffprobe", "-v", "quiet", "-show_entries", "format=duration", "-of", "json", filepath], capture_output=True, text=True, timeout=30 ) if result.returncode == 0: data = json.loads(result.stdout) return float(data["format"]["duration"]) except Exception as e: print(f" 获取音频时长失败 {filepath}: {e}") return 0 def test_result_consistency(): # 假设已运行过一次批量任务,输出在 outputs/ 目录下 import glob import os output_dir = "/root/workspace/outputs" video_files = glob.glob(os.path.join(output_dir, "*.mp4")) if len(video_files) < 5: print("❌ 输出视频不足5个,跳过一致性校验") return False audio_duration = get_audio_duration("/root/workspace/test_audio.mp3") print(f" 参考音频时长:{audio_duration:.2f} 秒") all_passed = True for i, video in enumerate(video_files[:5]): # 校验前5个 size_mb = os.path.getsize(video) / (1024*1024) video_duration = get_video_duration(video) print(f"\n 视频 {i+1}: {os.path.basename(video)}") print(f" 大小:{size_mb:.1f} MB → {'' if size_mb > 1 else '❌'}") print(f" 时长:{video_duration:.2f} 秒 → {'' if abs(video_duration - audio_duration) < 0.5 else '❌'}") if size_mb < 1 or abs(video_duration - audio_duration) > 0.5: all_passed = False print(f" ❌ 质量基线未达标") if all_passed: print("\n 批量结果一致性校验全部通过") else: print("\n❌ 批量结果存在不一致问题,需检查生成逻辑") return all_passed test_result_consistency()为什么必须做?
- GPU显存波动可能导致某次生成帧率异常,视频时长缩短
- 存储IO瓶颈可能造成写入截断,生成半成品视频
- 不同批次任务可能因缓存未清理,复用旧模型参数
这是上线前的最后一道质量门禁,确保交付给客户的每个视频都达到基本可用标准。
总结:把5个测试变成你的上线Checklist
这5个自动化测试不是一次性任务,而是你应该固化进HeyGem交付流程的日常习惯:
- 测试1(服务存活)→ 加入Docker容器健康检查(
HEALTHCHECK指令) - 测试2(批量流程)→ 接入GitLab CI/CD,每次
git push后自动触发 - 测试3(单个健壮)→ 写入《客户部署手册》,作为现场实施必检项
- 测试4(异常恢复)→ 在压测报告中体现,证明系统韧性指标
- 测试5(结果一致)→ 作为版本发布前的最终门禁,未通过则禁止发布
它们共同构成了一张安全网:既防止低级失误(端口没开、按钮失效),也防范隐蔽风险(磁盘写满、结果漂移)。更重要的是,它们把“上线”这件事,从一场提心吊胆的赌博,变成一次有数据支撑的确定性行动。
当你下次收到客户“视频口型不同步”的反馈时,不再需要登录服务器翻日志、重放操作;你只需运行这5个脚本,3分钟内就能定位是环境问题、输入问题,还是模型本身缺陷。这种掌控感,正是专业工程实践的起点。
别再让上线变成救火现场。
现在就把这5个测试脚本,放进你的/root/workspace/tests/目录里。
--- > **获取更多AI镜像** > > 想探索更多AI镜像和应用场景?访问 [CSDN星图镜像广场](https://ai.csdn.net/?utm_source=mirror_blog_end),提供丰富的预置镜像,覆盖大模型推理、图像生成、视频生成、模型微调等多个领域,支持一键部署。