1
0

thumbnail_audit.py 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353
  1. #!/usr/bin/env python3
  2. """
  3. thumbnail_audit.py - 基于MrBeast缩略图理论的检查清单脚本
  4. 检查维度(基于MrBeast公开分享的缩略图原则):
  5. 1. 标题与缩略图互补性:是否「互补而非重复」?
  6. 2. 焦点数量:是否只有1-2个视觉焦点?
  7. 3. 文字量:缩略图文字是否少于5个词?
  8. 4. 情绪表达:是否有明确的面部表情/情绪?
  9. 5. 颜色对比:颜色对比是否足够醒目?
  10. 如果提供了图片文件,会用PIL分析颜色分布和亮度对比。
  11. 用法:
  12. python thumbnail_audit.py --title "I Spent 50 Hours Buried Alive"
  13. python thumbnail_audit.py --title "..." --image thumbnail.jpg
  14. python thumbnail_audit.py --title "..." --image thumbnail.jpg -o report.md
  15. 依赖: Pillow(仅图片分析时需要,纯文本检查无依赖)
  16. """
  17. import argparse
  18. import sys
  19. from pathlib import Path
  20. # ---------- MrBeast缩略图原则 ----------
  21. REDUNDANCY_WORDS = {
  22. # 如果标题中的关键词大量出现在缩略图文字中,说明重复而非互补
  23. "challenge", "survive", "hours", "days", "dollars", "money",
  24. "biggest", "world", "first", "last", "never", "impossible",
  25. }
  26. # 情绪相关词汇(用于标题分析)
  27. EMOTION_WORDS = [
  28. "shocked", "scared", "amazed", "crying", "screaming", "laughing",
  29. "surprised", "angry", "terrified", "excited", "happy", "sad",
  30. "emotional", "insane", "crazy", "unbelievable", "incredible",
  31. # 中文
  32. "震惊", "害怕", "惊讶", "哭", "尖叫", "笑", "疯狂", "不敢相信",
  33. ]
  34. def check_title_thumbnail_complementarity(title: str, thumb_text: str = "") -> dict:
  35. """检查标题与缩略图是否互补(而非重复)"""
  36. title_words = set(title.lower().split())
  37. thumb_words = set(thumb_text.lower().split()) if thumb_text else set()
  38. if not thumb_text:
  39. return {
  40. "score": 3,
  41. "max": 5,
  42. "note": "未提供缩略图文字,无法完整评估。建议:缩略图应补充标题没说的信息。",
  43. }
  44. overlap = title_words & thumb_words & REDUNDANCY_WORDS
  45. overlap_ratio = len(overlap) / max(len(thumb_words), 1)
  46. if overlap_ratio > 0.5:
  47. score = 1
  48. note = f"缩略图文字与标题高度重复(重复词: {', '.join(overlap)})。MrBeast原则:缩略图应该补充标题,而不是重复标题。"
  49. elif overlap_ratio > 0.2:
  50. score = 3
  51. note = f"有部分重复({', '.join(overlap)}),但还可以。考虑让缩略图传递标题没说的信息。"
  52. else:
  53. score = 5
  54. note = "标题与缩略图互补性好,各自传递不同信息。"
  55. return {"score": score, "max": 5, "note": note, "overlap": list(overlap)}
  56. def check_text_amount(thumb_text: str = "") -> dict:
  57. """检查缩略图文字量"""
  58. if not thumb_text:
  59. return {
  60. "score": 4,
  61. "max": 5,
  62. "word_count": 0,
  63. "note": "未提供缩略图文字。MrBeast的缩略图通常文字极少(0-3词)或不用文字。",
  64. }
  65. word_count = len(thumb_text.split())
  66. if word_count == 0:
  67. score, note = 5, "无文字,干净利落。"
  68. elif word_count <= 3:
  69. score, note = 5, f"仅{word_count}词,符合MrBeast标准。"
  70. elif word_count <= 5:
  71. score, note = 3, f"{word_count}词,接近上限。考虑精简到3词以内。"
  72. else:
  73. score, note = 1, f"{word_count}词,太多了!MrBeast缩略图极少超过3-5个词。文字越少,点击率越高。"
  74. return {"score": score, "max": 5, "word_count": word_count, "note": note}
  75. def check_emotion_in_title(title: str) -> dict:
  76. """检查标题是否暗示明确的情绪(间接评估缩略图情绪需求)"""
  77. title_lower = title.lower()
  78. found_emotions = [w for w in EMOTION_WORDS if w in title_lower]
  79. # 检查感叹号和问号
  80. has_exclamation = "!" in title or "?" in title or "!" in title
  81. has_question = "?" in title or "?" in title
  82. if found_emotions:
  83. score = 5
  84. note = f"标题有明确情绪暗示({', '.join(found_emotions[:3])})。缩略图应该用面部表情呼应这种情绪。"
  85. elif has_exclamation or has_question:
  86. score = 3
  87. note = "标题有情绪标点,但缺少明确情绪词。缩略图需要用面部表情补充情绪。"
  88. else:
  89. score = 2
  90. note = "标题情绪不明显。MrBeast原则:缩略图必须有一张表情夸张的人脸,或者明确的情绪视觉元素。"
  91. return {
  92. "score": score,
  93. "max": 5,
  94. "emotions_found": found_emotions,
  95. "note": note,
  96. }
  97. def check_title_curiosity_gap(title: str) -> dict:
  98. """检查标题是否制造好奇心缺口"""
  99. curiosity_patterns = [
  100. ("数字对比", ["vs", "versus", "$", "比"]),
  101. ("悬念词", ["secret", "mystery", "hidden", "never", "impossible", "秘密", "不可能"]),
  102. ("挑战框架", ["challenge", "survive", "last", "endure", "挑战", "坚持"]),
  103. ("极端词", ["world", "biggest", "smallest", "most", "least", "最大", "最小", "最"]),
  104. ("时间压力", ["hours", "days", "minutes", "seconds", "小时", "天", "分钟"]),
  105. ]
  106. found = []
  107. title_lower = title.lower()
  108. for pattern_name, keywords in curiosity_patterns:
  109. if any(k in title_lower for k in keywords):
  110. found.append(pattern_name)
  111. if len(found) >= 3:
  112. score, note = 5, f"标题有{len(found)}个好奇心元素({', '.join(found)}),非常强!"
  113. elif len(found) >= 2:
  114. score, note = 4, f"标题有{len(found)}个好奇心元素({', '.join(found)}),不错。"
  115. elif len(found) == 1:
  116. score, note = 3, f"标题有1个好奇心元素({found[0]}),可以更强。"
  117. else:
  118. score, note = 1, "标题缺少好奇心缺口。MrBeast标题通常至少包含2-3个好奇心元素。"
  119. return {"score": score, "max": 5, "patterns_found": found, "note": note}
  120. def analyze_image(image_path: str) -> dict:
  121. """用PIL分析图片的颜色和对比度"""
  122. try:
  123. from PIL import Image, ImageStat
  124. except ImportError:
  125. return {
  126. "available": False,
  127. "note": "Pillow未安装,跳过图片分析。安装: pip install Pillow",
  128. }
  129. path = Path(image_path)
  130. if not path.exists():
  131. return {"available": False, "note": f"图片文件不存在: {image_path}"}
  132. try:
  133. img = Image.open(path)
  134. except Exception as e:
  135. return {"available": False, "note": f"无法打开图片: {e}"}
  136. # 转换为RGB
  137. if img.mode != "RGB":
  138. img = img.convert("RGB")
  139. stat = ImageStat.Stat(img)
  140. width, height = img.size
  141. # 平均亮度
  142. avg_brightness = sum(stat.mean) / 3
  143. # 亮度标准差(对比度指标)
  144. avg_stddev = sum(stat.stddev) / 3
  145. # 颜色饱和度分析
  146. hsv_img = img.convert("HSV")
  147. hsv_stat = ImageStat.Stat(hsv_img)
  148. avg_saturation = hsv_stat.mean[1]
  149. # 主色调分析(简化版:取中心区域和边缘区域对比)
  150. center_crop = img.crop((width // 4, height // 4, 3 * width // 4, 3 * height // 4))
  151. center_stat = ImageStat.Stat(center_crop)
  152. center_brightness = sum(center_stat.mean) / 3
  153. # 评估
  154. results = {
  155. "available": True,
  156. "size": f"{width}x{height}",
  157. "brightness": {
  158. "average": round(avg_brightness, 1),
  159. "score": 5 if 80 < avg_brightness < 200 else 3 if 50 < avg_brightness < 230 else 1,
  160. "note": "亮度适中" if 80 < avg_brightness < 200 else "偏暗或偏亮",
  161. },
  162. "contrast": {
  163. "stddev": round(avg_stddev, 1),
  164. "score": 5 if avg_stddev > 60 else 3 if avg_stddev > 40 else 1,
  165. "note": "对比度强" if avg_stddev > 60 else "对比度中等" if avg_stddev > 40 else "对比度不足,缩略图在小尺寸下可能不够醒目",
  166. },
  167. "saturation": {
  168. "average": round(avg_saturation, 1),
  169. "score": 5 if avg_saturation > 100 else 3 if avg_saturation > 60 else 2,
  170. "note": "色彩饱和度高" if avg_saturation > 100 else "色彩饱和度中等" if avg_saturation > 60 else "色彩偏淡,考虑增加饱和度",
  171. },
  172. "center_focus": {
  173. "center_brightness": round(center_brightness, 1),
  174. "edge_contrast": round(abs(center_brightness - avg_brightness), 1),
  175. "note": "中心区域与边缘有明显对比" if abs(center_brightness - avg_brightness) > 15 else "中心与边缘对比不明显,焦点可能不够突出",
  176. },
  177. }
  178. return results
  179. def generate_report(title: str, thumb_text: str = "", image_path: str = None) -> str:
  180. """生成完整审核报告"""
  181. complementarity = check_title_thumbnail_complementarity(title, thumb_text)
  182. text_amount = check_text_amount(thumb_text)
  183. emotion = check_emotion_in_title(title)
  184. curiosity = check_title_curiosity_gap(title)
  185. image_analysis = analyze_image(image_path) if image_path else None
  186. # 计算总分
  187. scores = [complementarity["score"], text_amount["score"], emotion["score"], curiosity["score"]]
  188. if image_analysis and image_analysis.get("available"):
  189. scores.append(image_analysis["brightness"]["score"])
  190. scores.append(image_analysis["contrast"]["score"])
  191. scores.append(image_analysis["saturation"]["score"])
  192. total = sum(scores)
  193. max_total = len(scores) * 5
  194. lines = []
  195. lines.append("# 缩略图审核报告\n")
  196. lines.append(f"**标题**: {title}")
  197. if thumb_text:
  198. lines.append(f"**缩略图文字**: {thumb_text}")
  199. if image_path:
  200. lines.append(f"**图片**: {image_path}")
  201. lines.append(f"\n**总分**: {total}/{max_total} ({total/max_total*100:.0f}%)\n")
  202. # 评级
  203. pct = total / max_total * 100
  204. if pct >= 80:
  205. grade = "A - 优秀,点击率潜力高"
  206. elif pct >= 60:
  207. grade = "B - 良好,有优化空间"
  208. elif pct >= 40:
  209. grade = "C - 及格,需要重点改进"
  210. else:
  211. grade = "D - 需要重做"
  212. lines.append(f"**评级**: {grade}\n")
  213. # 各项检查
  214. lines.append("## 1. 标题-缩略图互补性 ({}/{})\n".format(complementarity["score"], complementarity["max"]))
  215. lines.append(complementarity["note"])
  216. lines.append("")
  217. lines.append("## 2. 缩略图文字量 ({}/{})\n".format(text_amount["score"], text_amount["max"]))
  218. lines.append(text_amount["note"])
  219. lines.append("")
  220. lines.append("## 3. 情绪表达 ({}/{})\n".format(emotion["score"], emotion["max"]))
  221. lines.append(emotion["note"])
  222. lines.append("")
  223. lines.append("## 4. 好奇心缺口 ({}/{})\n".format(curiosity["score"], curiosity["max"]))
  224. lines.append(curiosity["note"])
  225. lines.append("")
  226. # 图片分析
  227. if image_analysis:
  228. if image_analysis.get("available"):
  229. lines.append(f"## 5. 图片技术分析 (尺寸: {image_analysis['size']})\n")
  230. b = image_analysis["brightness"]
  231. c = image_analysis["contrast"]
  232. s = image_analysis["saturation"]
  233. cf = image_analysis["center_focus"]
  234. lines.append(f"- **亮度** ({b['score']}/5): 平均 {b['average']} - {b['note']}")
  235. lines.append(f"- **对比度** ({c['score']}/5): 标准差 {c['stddev']} - {c['note']}")
  236. lines.append(f"- **饱和度** ({s['score']}/5): 平均 {s['average']} - {s['note']}")
  237. lines.append(f"- **焦点**: 中心-边缘差 {cf['edge_contrast']} - {cf['note']}")
  238. else:
  239. lines.append(f"## 5. 图片分析\n")
  240. lines.append(f"跳过: {image_analysis['note']}")
  241. lines.append("")
  242. # MrBeast缩略图清单
  243. lines.append("## MrBeast缩略图黄金法则\n")
  244. lines.append("- [ ] 缩略图在手机小屏上是否清晰可辨?")
  245. lines.append("- [ ] 是否只有1-2个视觉焦点(不杂乱)?")
  246. lines.append("- [ ] 是否有一张情绪强烈的人脸?")
  247. lines.append("- [ ] 缩略图是否让人产生「我必须点进去看」的冲动?")
  248. lines.append("- [ ] 标题和缩略图组合是否创造了信息缺口?")
  249. lines.append("- [ ] 与同时段其他视频放在一起时是否够醒目?")
  250. lines.append("")
  251. # 改进建议
  252. lines.append("## 改进建议\n")
  253. suggestions = []
  254. if complementarity["score"] < 4:
  255. suggestions.append("让缩略图传递标题没说的信息(比如标题说挑战,缩略图展示结果或最戏剧性的瞬间)")
  256. if text_amount["score"] < 4:
  257. suggestions.append("减少缩略图文字,理想是0-3个词,用视觉而非文字讲故事")
  258. if emotion["score"] < 4:
  259. suggestions.append("缩略图加入表情夸张的人脸照片,情绪越强烈越好")
  260. if curiosity["score"] < 4:
  261. suggestions.append("标题加入数字/极端词/时间压力等好奇心元素")
  262. if image_analysis and image_analysis.get("available"):
  263. if image_analysis["contrast"]["score"] < 4:
  264. suggestions.append("提高图片对比度,确保缩略图在小尺寸下也清晰醒目")
  265. if image_analysis["saturation"]["score"] < 4:
  266. suggestions.append("增加色彩饱和度,让图片在YouTube首页中跳出来")
  267. if suggestions:
  268. for i, s in enumerate(suggestions, 1):
  269. lines.append(f"{i}. {s}")
  270. else:
  271. lines.append("整体表现优秀,继续保持!")
  272. return "\n".join(lines)
  273. def main():
  274. parser = argparse.ArgumentParser(
  275. description="基于MrBeast缩略图理论的审核工具",
  276. formatter_class=argparse.RawDescriptionHelpFormatter,
  277. epilog='示例:\n python thumbnail_audit.py --title "I Survived 50 Hours In Antarctica"\n python thumbnail_audit.py --title "..." --thumb-text "50 HOURS" --image thumb.jpg',
  278. )
  279. parser.add_argument("--title", required=True, help="视频标题")
  280. parser.add_argument("--thumb-text", default="", help="缩略图上的文字(如果有)")
  281. parser.add_argument("--image", help="缩略图图片文件路径(可选)")
  282. parser.add_argument("-o", "--output", help="输出报告文件路径")
  283. args = parser.parse_args()
  284. report = generate_report(args.title, args.thumb_text, args.image)
  285. if args.output:
  286. Path(args.output).write_text(report, encoding="utf-8")
  287. print(f"[OK] 报告已保存到: {args.output}")
  288. else:
  289. print(report)
  290. if __name__ == "__main__":
  291. main()