视频生产质量门禁 — 7道关卡自动化检查,全部通过才交付。覆盖HTML结构、时间线、TTS朗读、时长匹配、音频质量、动画执行、视觉抽检。
视频质量门禁 — 7道关卡是一项面向实际任务的技能,主要用于视频生产完成后,必须依次通过7道关卡检查;任一关卡失败,立即停止并告警,不得交付;
MOSZE;Slogan;手机配件一站式批发;2563eb(蓝色);06b6d4(青色)、#fbbf24(黄色)、#10b981(绿色)、#f44336(红色);Inter, -a。它将相关步骤、工具调用和结果整理方式集中到统一流程中,帮助使用者更快完成目标并减少重复操作。
从功能定位来看,该技能强调把分散的操作要求整理成清晰、可复用的处理流程,使用户能够围绕既定目标快速准备输入、选择执行方式并获得结构化结果。实际使用前应先确认任务范围、数据来源、运行环境、必要权限和关键参数,再依据技能说明逐步执行;若输入条件不完整,应先补齐信息或采用保守配置,避免因错误假设导致结果偏离需求。执行过程中需要关注工具调用是否成功、接口或依赖是否可用、输出格式是否符合预期,并对异常提示、缺失字段和边界情况进行处理;涉及批量任务时,还应保存进度,避免中断后重复操作。该技能适合用于一次性任务,也可以接入自动化工作流,与其他技能或上层代理配合完成更完整的业务链路;在组合使用时,应明确每一步的输入输出关系,并避免不同步骤之间出现参数冲突。
视频生产完成后,必须依次通过7道关卡检查。任一关卡失败,立即停止并告警,不得交付。
所有模世界视频必须遵循以下标准:
| 项目 | 标准值 |
|---|---|
| 品牌名 | 模世界(禁止写成"膜世界") |
| 英文名 | MOSZE |
| Slogan | 手机配件一站式批发 |
| 主色调 | #2563eb(蓝色) |
| 辅助色 | #06b6d4(青色)、#fbbf24(黄色)、#10b981(绿色)、#f44336(红色) |
| 字体 | Inter, -apple-system, Helvetica Neue, Arial |
| 分辨率 | 1080×1920(竖屏) |
| 时长 | 25秒 |
| Logo位置 | 底部居中,高度70px |
| 场景数 | 4个(s1钩子 → s2参数 → s3卖点 → s4品牌) |
时机: HTML文件生成后立即执行
方法: 代码级静态检查
def gate0_html_structure(html_content: str) -> dict:
"""检查HTML文件结构完整性"""
results = []
# 1. doctype存在
if not html_content.strip().upper().startswith(''):
results.append(('FAIL', '缺少声明'))
return {'pass': False, 'details': results}
# 2. 存在
if '' not in html_content:
results.append(('FAIL', '缺少容器'))
return {'pass': False, 'details': results}
# 3. GSAP script存在
if 'gsap.timeline' not in html_content:
results.append(('FAIL', '缺少GSAP时间线代码'))
return {'pass': False, 'details': results}
# 4. window.__timelines注册存在
if 'window.__timelines' not in html_content:
results.append(('FAIL', '缺少window.__timelines注册'))
return {'pass': False, 'details': results}
# 5. 4个场景(支持两种写法:id="s1" 或 class="scene s1")
for scene_num in ['1', '2', '3', '4']:
has_id = f'id="s{scene_num}"' in html
has_class = f'class="scene s{scene_num}"' in html
if not has_id and not has_class:
results.append(('FAIL', f'缺少场景 s{scene_num}'))
return {'pass': False, 'details': results}
# 6. 选择器格式正确(#root .s1 而非 .s1)
import re
gsap_calls = re.findall(r'#root .sd+', html_content)
if not gsap_calls:
results.append(('WARN', '未使用#root .s1选择器,动画可能不生效'))
# 7. duration属性匹配
if 'data-duration="25"' not in html_content:
results.append(('FAIL', 'data-duration不是25秒'))
return {'pass': False, 'details': results}
# 8. 品牌名检查
if '膜世界' in html_content:
results.append(('FAIL', '发现错别字"膜世界",应为"模世界"'))
return {'pass': False, 'details': results}
results.append(('PASS', 'HTML结构检查通过'))
return {'pass': True, 'details': results}
关卡0.5:时间线验证
时机: HTML结构检查通过后执行
方法: 模拟执行GSAP时间线,验证动画有效性
检查项
def gate05_timeline_validation(html_content: str) -> dict:
"""验证GSAP时间线是否有效"""
results = []
# 1. 提取所有tl.to()调用
import re
to_calls = re.findall(r'.to([^)]+)', html_content)
if len(to_calls) < 4:
results.append(('FAIL', f'GSAP .to()调用过少({len(to_calls)}个,至少需要4个场景动画)'))
return {'pass': False, 'details': results}
# 2. 每个场景是否有动画(支持 #root .s1 和 .s1 两种写法)
scenes_with_anim = set()
for call in to_calls:
for scene_num in ['1', '2', '3', '4']:
if f'.s{scene_num}' in call:
scenes_with_anim.add(f'.s{scene_num}')
missing = set(['.s1', '.s2', '.s3', '.s4']) - scenes_with_anim
if missing:
results.append(('FAIL', f'以下场景无动画:{missing}'))
return {'pass': False, 'details': results}
# 3. 检查时间线总时长(通过解析时间戳估算)
# 提取所有时间点
time_points = re.findall(r'},s*(d+.?d*)s*)', html_content)
if time_points:
max_time = max(float(t) for t in time_points)
if max_time < 20:
results.append(('FAIL', f'时间线最大时间仅{max_time}s,不足25s'))
return {'pass': False, 'details': results}
if max_time > 30:
results.append(('WARN', f'时间线最大时间{max_time}s,超过25s'))
# 4. 检查是否有重复注册同一场景
scene_registrations = re.findall(r'set('#root .s(d)'', html_content)
from collections import Counter
counts = Counter(scene_registrations)
for scene, count in counts.items():
if count > 1:
results.append(('WARN', f'场景s{scene}被注册{count}次'))
results.append(('PASS', '时间线验证通过'))
return {'pass': True, 'details': results}
关卡1:脚本内容检查
时机: 生成配音前执行
方法: 文本扫描 + TTS友好度检测
检查项
def gate1_script_content(texts: list, html_content: str) -> dict:
"""检查配音文本和HTML内容"""
results = []
# 1. 错别字扫描
forbidden = {
'膜世界': '模世界',
'精钻膜': '晶钻膜',
'防窥uvm': '防窥UV膜',
'壳磁': '壳瓷',
'电镀丝滑mo': '电镀丝滑膜',
}
all_text = ' '.join([t[2] for t in texts]) + html_content
for wrong, correct in forbidden.items():
if wrong in all_text:
results.append(('FAIL', f'发现错别字"{wrong}",应为"{correct}"'))
return {'pass': False, 'details': results}
# 2. 品牌名统一检查
if '鑫源光电' in html_content and '模世界' not in html_content:
results.append(('WARN', 'HTML中使用鑫源光电但未使用模世界,请确认品牌'))
# 3. 参数合理性
# 厚度检查
thickness_match = re.search(r'(d+.?d*)s*(mm|厘米|m)', all_text)
if thickness_match:
val = float(thickness_match.group(1))
unit = thickness_match.group(2)
if unit == '厘米' and val > 0.1:
results.append(('FAIL', f'厚度{val}厘米不合理,应为{val*10}mm'))
return {'pass': False, 'details': results}
# 透光率检查
trans_match = re.search(r'(d+.?d*)%', all_text)
if trans_match:
val = float(trans_match.group(1))
if val > 100:
results.append(('FAIL', f'透光率{val}%超过100%,不合理'))
return {'pass': False, 'details': results}
# 4. 广告法风险词
risk_words = ['最', '第一', '绝对', '顶级', '国家级', '独家']
for word in risk_words:
if word in all_text:
results.append(('WARN', f'发现广告法风险词"{word}",建议删除或修改'))
# 5. TTS友好度检测
tts_issues = []
# 检查数字+字母组合
num_alpha = re.findall(r'(d+[A-Z]+)', all_text)
for na in num_alpha:
tts_issues.append(f'"{na}"可能被TTS误读(如9H读成九H)')
# 检查小数点
decimals = re.findall(r'(d+.d+)', all_text)
for d in decimals:
tts_issues.append(f'"{d}"可能被TTS读成"零点...",建议确认是否需汉字')
if tts_issues:
results.append(('WARN', f'TTS朗读潜在问题:{len(tts_issues)}处'))
for issue in tts_issues[:3]:
results.append(('INFO', issue))
# 6. 标点符号检查(逗号=短停,句号=长停)
sentences = re.split(r'[。!?]', all_text)
for s in sentences:
if len(s) > 50:
results.append(('WARN', f'句子过长({len(s)}字),建议拆分'))
results.append(('PASS', '脚本内容检查通过'))
return {'pass': True, 'details': results}
关卡2:时长匹配检查
时机: 配音生成后、渲染前执行
方法: 对比配音时长与场景动画时间窗口
检查项
def gate2_duration_match(audio_files: dict, html_content: str) -> dict:
"""检查配音时长与场景时间窗口是否匹配"""
results = []
# 1. 获取每个音频文件的实际时长
import subprocess
audio_durations = {}
for name, path in audio_files.items():
if os.path.exists(path):
cmd = ['ffprobe', '-v', 'error', '-show_entries', 'format=duration',
'-of', 'default=noprint_wrappers=1:nokey=1', path]
proc = subprocess.run(cmd, capture_output=True, text=True, timeout=10)
if proc.returncode == 0:
audio_durations[name] = float(proc.stdout.strip())
# 2. 提取HTML中每个场景的时间窗口
# s1: 0-5s, s2: 5-13s, s3: 13-20s, s4: 20-25s
scene_windows = {
's1': (0, 5),
's2': (5, 13),
's3': (13, 20),
's4': (20, 25),
}
# 3. 对比音频时长与场景窗口
for name, duration in audio_durations.items():
scene_num = name[1] # s1 -> 1
scene_id = f's{scene_num}'
if scene_id in scene_windows:
start, end = scene_windows[scene_id]
window = end - start
if duration > window + 2: # 允许2秒缓冲
results.append(('FAIL', f'{name}时长{duration:.1f}s超过场景窗口{window}s(超限{duration-window:.1f}s)'))
return {'pass': False, 'details': results}
elif duration < window - 5: # 少于窗口5秒告警
results.append(('WARN', f'{name}时长{duration:.1f}s远小于场景窗口{window}s'))
# 4. 总时长检查
total_audio = sum(audio_durations.values())
if total_audio > 27: # 25秒+2秒缓冲
results.append(('WARN', f'总配音时长{total_audio:.1f}s,接近或超过25秒'))
results.append(('PASS', '时长匹配检查通过'))
return {'pass': True, 'details': results}
关卡2.5:音频质量检查
时机: 渲染前执行
方法: ffprobe检测静音、爆音、音量突变
检查项
def gate25_audio_quality(audio_files: dict) -> dict:
"""检查音频文件质量"""
results = []
import subprocess
for name, path in audio_files.items():
if not os.path.exists(path):
results.append(('FAIL', f'{name}文件不存在'))
return {'pass': False, 'details': results}
# 1. 检测开头静音(前0.5秒应有声音)
cmd = ['ffprobe', '-v', 'error', '-show_entries', 'format_tags=loudness',
'-of', 'default=noprint_wrappers=1:nokey=1', path]
proc = subprocess.run(cmd, capture_output=True, text=True, timeout=10)
# 2. 检测总体音量(RMS)
cmd = ['ffmpeg', '-i', path, '-af', 'astats=metadata=1:reset=0',
'-f', 'null', '-']
proc = subprocess.run(cmd, capture_output=True, text=True, timeout=10)
stderr = proc.stderr
# 检查是否有静音段(RMS < -60dB)
if 'Flat factor' in stderr:
# 提取RMS值
import re
rms_match = re.search(r'RMS level db: (-?d+.?d*)', stderr)
if rms_match:
rms = float(rms_match.group(1))
if rms < -60:
results.append(('WARN', f'{name}平均音量过低({rms}dB)'))
# 3. 检测文件大小合理性
size = os.path.getsize(path)
if size < 1000: # 小于1KB通常为静音或空文件
results.append(('FAIL', f'{name}文件大小仅{size}字节,可能为空或静音'))
return {'pass': False, 'details': results}
results.append(('PASS', '音频质量检查通过'))
return {'pass': True, 'details': results}
关卡3:渲染结果检查
时机: 视频渲染完成后执行
方法: 文件检查 + 像素对比验证动画执行
检查项
def gate3_render_result(video_path: str, audio_path: str) -> dict:
"""检查渲染结果"""
results = []
import subprocess
import os
# 1. 文件存在性
if not os.path.exists(video_path):
results.append(('FAIL', '渲染视频文件不存在'))
return {'pass': False, 'details': results}
# 2. 文件大小检查
size = os.path.getsize(video_path)
if size < 10000: # 小于10KB异常
results.append(('FAIL', f'视频文件仅{size}字节,渲染可能失败'))
return {'pass': False, 'details': results}
# 3. 视频时长检查
cmd = ['ffprobe', '-v', 'error', '-show_entries', 'format=duration',
'-of', 'default=noprint_wrappers=1:nokey=1', video_path]
proc = subprocess.run(cmd, capture_output=True, text=True, timeout=10)
if proc.returncode == 0:
duration = float(proc.stdout.strip())
if abs(duration - 25) > 2: # 允许2秒误差
results.append(('FAIL', f'视频时长{duration:.1f}s,预期25s'))
return {'pass': False, 'details': results}
# 4. 音频合并检查
if os.path.exists(audio_path):
cmd = ['ffprobe', '-v', 'error', '-select_streams', 'a:0',
'-show_entries', 'stream=codec_type',
'-of', 'default=noprint_wrappers=1:nokey=1', video_path]
proc = subprocess.run(cmd, capture_output=True, text=True, timeout=10)
if 'audio' not in proc.stdout:
results.append(('FAIL', '视频文件中无音频轨道'))
return {'pass': False, 'details': results}
# 5. 动画执行验证(首帧vs中帧像素差异)
# 截取0s帧和12.5s帧
frame0 = video_path.replace('.mp4', '_frame0.png')
frame_mid = video_path.replace('.mp4', '_frame_mid.png')
subprocess.run(['ffmpeg', '-y', '-ss', '0', '-i', video_path,
'-frames:v', '1', frame0], capture_output=True, timeout=30)
subprocess.run(['ffmpeg', '-y', '-ss', '12.5', '-i', video_path,
'-frames:v', '1', frame_mid], capture_output=True, timeout=30)
if os.path.exists(frame0) and os.path.exists(frame_mid):
# 计算像素差异
import cv2
img0 = cv2.imread(frame0)
img_mid = cv2.imread(frame_mid)
if img0 is not None and img_mid is not None:
diff = cv2.absdiff(img0, img_mid)
diff_ratio = np.count_nonzero(diff) / (img0.size / 3)
if diff_ratio < 0.05: # 差异小于5%
results.append(('FAIL', '动画可能未执行(首帧与中帧差异<5%)'))
return {'pass': False, 'details': results}
results.append(('PASS', '渲染结果检查通过'))
return {'pass': True, 'details': results}
关卡4:视觉抽检
时机: 所有关卡通过后执行
方法: 截取关键帧,用vision模型检查画面
检查项
def gate4_visual_review(video_path: str) -> dict:
"""视觉抽检 — 关键帧画面检查"""
results = []
# 截取5个关键帧
keyframes = {
'0s': '0',
'5s': '5',
'13s': '13',
'20s': '20',
'24s': '24',
}
frame_paths = {}
for name, time_str in keyframes.items():
frame_path = video_path.replace('.mp4', f'_kf_{name}.png')
subprocess.run(['ffmpeg', '-y', '-ss', time_str, '-i', video_path,
'-frames:v', '1', frame_path], capture_output=True, timeout=30)
if os.path.exists(frame_path):
frame_paths[name] = frame_path
# 调用vision模型检查
# 注意:这里需要vision工具,在skill执行时动态调用
results.append(('INFO', f'已截取{len(frame_paths)}个关键帧'))
results.append(('INFO', '请使用vision工具检查以下关键帧:'))
for name, path in frame_paths.items():
results.append(('INFO', f' {name}: {path}'))
# 检查项(人工+vision):
# 1. 文字是否清晰可读
# 2. 品牌名是否正确(模世界)
# 3. Logo是否完整显示
# 4. 颜色是否符合品牌标准
# 5. 是否有明显排版错误
results.append(('PASS', '视觉抽检完成(请人工确认)'))
return {'pass': True, 'details': results}
执行流程
视频生成完成
│
▼
关卡0:HTML结构检查 ──── 失败 → 停止,返回错误
│ 通过
▼
关卡0.5:时间线验证 ──── 失败 → 停止,返回错误
│ 通过
▼
关卡1:脚本内容检查 ──── 失败 → 停止,返回错误
│ 通过(WARN允许继续)
▼
关卡2:时长匹配检查 ──── 失败 → 停止,返回错误
│ 通过
▼
关卡2.5:音频质量检查 ── 失败 → 停止,返回错误
│ 通过
▼
关卡3:渲染结果检查 ──── 失败 → 停止,返回错误
│ 通过
▼
关卡4:视觉抽检 ──────── 人工确认
│
▼
交付视频
告警规则
- FAIL:必须修复,不得跳过
- WARN:记录问题,可继续但需在交付报告中说明
- INFO:提示信息,不影响流程
交付报告格式
## 视频质量门禁报告
- 关卡0 HTML结构:✅ 通过
- 关卡0.5 时间线验证:✅ 通过
- 关卡1 脚本内容:⚠️ WARN(TTS朗读问题3处)
- 关卡2 时长匹配:✅ 通过
- 关卡2.5 音频质量:✅ 通过
- 关卡3 渲染结果:✅ 通过
- 关卡4 视觉抽检:✅ 通过
总耗时:X秒
注意事项
- 关卡0-2.5是自动化检查,无需人工干预
- 关卡3的像素对比需要opencv-python,如未安装则跳过
- 关卡4需要vision模型,如不可用则改为人工检查
- 任一关卡FAIL,立即停止流程,不得交付
- 品牌名"模世界"是最高优先级检查,发现"膜世界"直接FAIL
关联文件
references/integration-matrix.md — 所有引用本skill的视频生产流程清单
scripts/quality_gate.py — 自动化执行脚本
Pitfalls(实战经验)
音频截断陷阱(2026-07-08发现 ⭐)
症状:视频文件存在、时长正确、有音频轨道,但实际时长只有21秒而不是25秒。
原因:ffmpeg合成时用了-shortest参数,音频只有21秒所以视频被截断到21秒。
修复:配音短于25秒时,用ffmpeg -af "apad=pad_dur=X"填充到25秒,再用-shortest合成。
# 错误做法:直接 -shortest
ffmpeg -i video.mp4 -i audio.m4a -c:v copy -shortest final.mp4 # 可能被截断
# 正确做法:先填充音频
AUDIO_DUR=$(ffprobe -v error -show_entries format=duration -of default=noprint_wrappers=1:nokey=1 audio.m4a)
if (( $(echo "$AUDIO_DUR < 23" | bc -l 2>/dev/null || echo 0) )); then
ffmpeg -y -i audio.m4a -af "apad=pad_dur=$((25-${AUDIO_DUR%.*}))" -t 25 audio_padded.m4a
ffmpeg -y -i video.mp4 -i audio_padded.m4a -c:v copy -c:a aac -b:a 192k -shortest final.mp4
else
ffmpeg -y -i video.mp4 -i audio.m4a -c:v copy -c:a aac -b:a 192k -shortest final.mp4
fi
场景选择器兼容(2026-07-08发现)
V7模板使用class="scene s1"而非id="s1"。关卡0和关卡0.5必须同时检查两种写法。
TTS误报(2026-07-08发现)
关卡1的TTS友好度检测会扫描整个HTML(包括CSS类名如6B、4ECDC),产生大量误报。实际只需检查配音文本文件,不应扫描HTML源码中的class/id。
相关专题
更多
热门下载
更多
相关下载
更多
精品课程
更多
热门推荐 /
最新课程
phpStudy极速入门视频教程
共6课时 | 54.7万人学习
独孤九贱(4)_PHP视频教程
共89课时 | 134.1万人学习
PHP实战天龙八部之仿爱奇艺电影网站
共49课时 | 82.6万人学习