You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
205 lines
7.4 KiB
205 lines
7.4 KiB
#!/usr/bin/env python3
|
|
"""Seedance 视频片段质量审查模板。
|
|
|
|
此脚本不直接调用 API,而是:
|
|
1. 输出标准化的 read_media 审查 prompt
|
|
2. 解析审查结果 JSON 并给出 PASS/FAIL 判定
|
|
3. 将审查记录写入 review_log.json
|
|
|
|
使用方式:
|
|
# 生成审查 prompt(供 Claude 调用 read_media 时使用)
|
|
python3 review_segment.py prompt <segment.mp4>
|
|
|
|
# 记录审查结果
|
|
python3 review_segment.py record <segment.mp4> <PASS|FAIL> <reason> [--log review_log.json]
|
|
|
|
# 检查所有片段是否通过审查
|
|
python3 review_segment.py check [--log review_log.json]
|
|
"""
|
|
|
|
import json
|
|
import sys
|
|
from datetime import datetime
|
|
from pathlib import Path
|
|
|
|
# ═══════════════════════════════════════════
|
|
# 审查标准定义
|
|
# ═══════════════════════════════════════════
|
|
|
|
REVIEW_CRITERIA = {
|
|
"style": {
|
|
"name": "风格一致性",
|
|
"name_en": "Style Consistency",
|
|
"description": "必须是 3D 皮克斯风格动画,不能是 2D 扁平/写实/其他风格",
|
|
"weight": "CRITICAL", # CRITICAL = 不通过即 FAIL
|
|
},
|
|
"camera": {
|
|
"name": "分镜切换",
|
|
"name_en": "Camera Transitions",
|
|
"description": "15s 片段内必须有至少 2 次镜头切换(全景↔特写),不能全程固定机位",
|
|
"weight": "CRITICAL",
|
|
},
|
|
"audio": {
|
|
"name": "音频质量",
|
|
"name_en": "Audio Quality",
|
|
"description": "对话清晰无乱码、无重叠/重复/AI 噪声,全程可懂",
|
|
"weight": "CRITICAL",
|
|
},
|
|
"character": {
|
|
"name": "角色一致性",
|
|
"name_en": "Character Consistency",
|
|
"description": "角色外观(服装、配饰、体型)与参考图一致",
|
|
"weight": "IMPORTANT", # IMPORTANT = 警告但可酌情通过
|
|
},
|
|
"content": {
|
|
"name": "内容匹配",
|
|
"name_en": "Content Match",
|
|
"description": "对话内容与脚本大致吻合,不必逐字匹配但意思要对",
|
|
"weight": "IMPORTANT",
|
|
},
|
|
}
|
|
|
|
# ═══════════════════════════════════════════
|
|
# 标准化审查 Prompt
|
|
# ═══════════════════════════════════════════
|
|
|
|
REVIEW_PROMPT = """Evaluate this video segment against these 5 criteria. For each criterion, give a PASS or FAIL verdict with a brief reason.
|
|
|
|
## Criteria
|
|
|
|
1. **STYLE** (Critical): Is this 3D Pixar-style animation? NOT 2D flat/realistic/other styles. Rate: PASS or FAIL.
|
|
|
|
2. **CAMERA TRANSITIONS** (Critical): Count the number of distinct camera angle changes (e.g. wide shot → close-up → wide shot). A 15s segment MUST have at least 2 camera changes. List each shot with timestamp. Rate: PASS or FAIL.
|
|
|
|
3. **AUDIO QUALITY** (Critical): Is ALL dialogue clear and understandable throughout the entire clip? Check for: garbled speech, overlapping/doubled audio tracks, AI noise/artifacts, nonsensical words. Even a brief 2-second garble = FAIL. Rate: PASS or FAIL.
|
|
|
|
4. **CHARACTER CONSISTENCY** (Important): Do characters maintain consistent appearance (clothes, accessories, body type) across shots? Rate: PASS or FAIL.
|
|
|
|
5. **CONTENT MATCH** (Important): Does the spoken dialogue roughly match expected content? (Don't need exact words, but the topic/meaning should be right.) Rate: PASS or FAIL.
|
|
|
|
## Output Format
|
|
|
|
Respond in this exact JSON format:
|
|
```json
|
|
{
|
|
"style": {"verdict": "PASS/FAIL", "reason": "..."},
|
|
"camera": {"verdict": "PASS/FAIL", "reason": "...", "shot_count": N, "shots": ["0:00-0:05 wide shot", ...]},
|
|
"audio": {"verdict": "PASS/FAIL", "reason": "...", "garbled_sections": []},
|
|
"character": {"verdict": "PASS/FAIL", "reason": "..."},
|
|
"content": {"verdict": "PASS/FAIL", "reason": "..."},
|
|
"overall": "PASS/FAIL",
|
|
"summary": "One sentence summary"
|
|
}
|
|
```
|
|
|
|
OVERALL is PASS only if ALL Critical criteria (style, camera, audio) pass. Important criteria failures are warnings but don't block overall PASS."""
|
|
|
|
|
|
def cmd_prompt(segment_path: str):
|
|
"""输出标准化的审查 prompt。"""
|
|
print("=" * 60)
|
|
print(f"审查片段: {segment_path}")
|
|
print("=" * 60)
|
|
print()
|
|
print("请使用以下 prompt 调用 read_media:")
|
|
print()
|
|
print(REVIEW_PROMPT)
|
|
print()
|
|
print("审查标准:")
|
|
for key, c in REVIEW_CRITERIA.items():
|
|
print(f" [{c['weight']}] {c['name']} ({c['name_en']}): {c['description']}")
|
|
|
|
|
|
def cmd_record(segment_path: str, verdict: str, reason: str, log_path: str = "review_log.json"):
|
|
"""记录审查结果到 JSON 日志。"""
|
|
log_file = Path(log_path)
|
|
if log_file.exists():
|
|
log = json.loads(log_file.read_text())
|
|
else:
|
|
log = {"reviews": [], "segments": {}}
|
|
|
|
segment_name = Path(segment_path).name
|
|
entry = {
|
|
"segment": segment_name,
|
|
"path": str(segment_path),
|
|
"verdict": verdict.upper(),
|
|
"reason": reason,
|
|
"timestamp": datetime.now().isoformat(),
|
|
}
|
|
log["reviews"].append(entry)
|
|
log["segments"][segment_name] = entry
|
|
|
|
log_file.write_text(json.dumps(log, indent=2, ensure_ascii=False))
|
|
icon = "✅" if verdict.upper() == "PASS" else "❌"
|
|
print(f"{icon} {segment_name}: {verdict.upper()} — {reason}")
|
|
|
|
|
|
def cmd_check(log_path: str = "review_log.json"):
|
|
"""检查所有片段审查状态。"""
|
|
log_file = Path(log_path)
|
|
if not log_file.exists():
|
|
print("未找到审查日志,请先进行审查")
|
|
sys.exit(1)
|
|
|
|
log = json.loads(log_file.read_text())
|
|
segments = log.get("segments", {})
|
|
|
|
if not segments:
|
|
print("无审查记录")
|
|
sys.exit(1)
|
|
|
|
all_pass = True
|
|
print("=== 审查状态汇总 ===\n")
|
|
for name, entry in sorted(segments.items()):
|
|
icon = "✅" if entry["verdict"] == "PASS" else "❌"
|
|
print(f" {icon} {name}: {entry['verdict']} — {entry['reason']}")
|
|
if entry["verdict"] != "PASS":
|
|
all_pass = False
|
|
|
|
print()
|
|
if all_pass:
|
|
print("ALL PASS — 所有片段通过审查,可以进行拼接合成")
|
|
else:
|
|
failed = [n for n, e in segments.items() if e["verdict"] != "PASS"]
|
|
print(f"BLOCKED — {len(failed)} 个片段未通过: {', '.join(failed)}")
|
|
sys.exit(1)
|
|
|
|
|
|
def main():
|
|
if len(sys.argv) < 2:
|
|
print(__doc__)
|
|
sys.exit(1)
|
|
|
|
cmd = sys.argv[1]
|
|
|
|
if cmd == "prompt":
|
|
if len(sys.argv) < 3:
|
|
print("用法: python review_segment.py prompt <segment.mp4>")
|
|
sys.exit(1)
|
|
cmd_prompt(sys.argv[2])
|
|
|
|
elif cmd == "record":
|
|
if len(sys.argv) < 5:
|
|
print("用法: python review_segment.py record <segment.mp4> <PASS|FAIL> <reason>")
|
|
sys.exit(1)
|
|
log_path = "review_log.json"
|
|
for i, arg in enumerate(sys.argv):
|
|
if arg == "--log" and i + 1 < len(sys.argv):
|
|
log_path = sys.argv[i + 1]
|
|
cmd_record(sys.argv[2], sys.argv[3], sys.argv[4], log_path)
|
|
|
|
elif cmd == "check":
|
|
log_path = "review_log.json"
|
|
for i, arg in enumerate(sys.argv):
|
|
if arg == "--log" and i + 1 < len(sys.argv):
|
|
log_path = sys.argv[i + 1]
|
|
cmd_check(log_path)
|
|
|
|
else:
|
|
print(f"未知命令: {cmd}")
|
|
print(__doc__)
|
|
sys.exit(1)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|
|
|