basic.py 9.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217
  1. import os
  2. import re
  3. import json
  4. import time
  5. import datetime
  6. import requests
  7. from typing import Optional, Dict
  8. from requests.exceptions import RequestException
  9. from tenacity import retry
  10. from applications.db import DatabaseConnector
  11. from applications.utils import request_retry
  12. retry_desc = request_retry(retry_times=3, min_retry_delay=2, max_retry_delay=30)
  13. def get_status_field_by_task(task: str) -> tuple[str, str]:
  14. match task:
  15. case "upload":
  16. status = "upload_status"
  17. update_timestamp = "upload_status_ts"
  18. case "extract":
  19. status = "extract_status"
  20. update_timestamp = "extract_status_ts"
  21. case "get_cover":
  22. status = "get_cover_status"
  23. update_timestamp = "get_cover_status_ts"
  24. case _:
  25. raise ValueError(f"Unexpected task: {task}")
  26. return status, update_timestamp
  27. def roll_back_lock_tasks(
  28. db_client: DatabaseConnector,
  29. task: str,
  30. max_process_time: int,
  31. init_status: int,
  32. processing_status: int,
  33. ) -> int:
  34. """
  35. rollback tasks which have been locked for a long time
  36. """
  37. status, update_timestamp = get_status_field_by_task(task)
  38. now_timestamp = int(time.time())
  39. timestamp_threshold = datetime.datetime.fromtimestamp(
  40. now_timestamp - max_process_time
  41. )
  42. update_query = f"""
  43. update long_articles_new_video_cover
  44. set {status} = %s
  45. where {status} = %s and {update_timestamp} < %s;
  46. """
  47. rollback_rows = db_client.save(
  48. query=update_query, params=(init_status, processing_status, timestamp_threshold)
  49. )
  50. return rollback_rows
  51. def download_file(task_id, oss_path):
  52. """
  53. 下载视频文件
  54. """
  55. video_url = "https://rescdn.yishihui.com/" + oss_path
  56. file_name = "static/{}.mp4".format(task_id)
  57. if os.path.exists(file_name):
  58. return file_name
  59. proxies = {"http": None, "https": None}
  60. with open(file_name, "wb") as f:
  61. response = requests.get(video_url, proxies=proxies)
  62. f.write(response.content)
  63. return file_name
  64. def update_task_queue_status(
  65. db_client: DatabaseConnector,
  66. task_id: int,
  67. task: str,
  68. ori_status: int,
  69. new_status: int,
  70. ) -> int:
  71. # update task queue status
  72. status, update_timestamp = get_status_field_by_task(task)
  73. update_query = f"""
  74. update long_articles_new_video_cover
  75. set {status} = %s, {update_timestamp} = %s
  76. where {status} = %s and id = %s;
  77. """
  78. update_rows = db_client.save(
  79. query=update_query,
  80. params=(
  81. new_status,
  82. datetime.datetime.now(),
  83. ori_status,
  84. task_id,
  85. ),
  86. )
  87. return update_rows
  88. def extract_best_frame_prompt():
  89. extract_prompt = """
  90. 以下为视频爆款封面的评分体系,请根据视频内容,逐帧分析视频画面,根据以下标准进行打分,最终先把分数最高的画面出现的时间输出给我,并精确到几时几分几秒几毫秒,格式:hh:mm:ss.xxx,要求是确切时间而不是时间段。
  91. 评分维度:
  92. 一、现实关切度 (满分15分):
  93. 高度相关(13-15分): 封面主题直接涉及老年人的生活、健康、财产等切身利益,例如养老、退休、健康、食品安全、子女教育、财产安全等。
  94. 中度相关(8-12分): 封面主题与老年人的生活有一定的关联,例如家长里短、邻里关系、社会热点、生活窍门等。
  95. 低度相关(4-7分): 封面主题与老年人的生活关系较弱,例如娱乐八卦、时尚潮流等。
  96. 无关(0-3分): 封面主题与老年人的生活基本无关。
  97. 二、社会认同感与亲切感 (满分15分):
  98. 高度认同(13-15分): 封面人物形象亲切、接地气,场景贴近老年人的生活,有强烈的代入感和归属感。
  99. 中度认同(8-12分): 封面人物形象尚可接受,场景有一定的熟悉感。
  100. 低度认同(4-7分): 封面人物形象陌生,场景较为遥远。
  101. 无认同感(0-3分): 封面人物形象令人反感,场景与老年人生活无关。
  102. 三、信息传达效率 (满分15分):
  103. 高效传达(13-15分): 封面文字简洁直白、字体较大、重点突出,视觉元素聚焦,能让老年人快速理解视频内容,色彩明快,对比强烈,视觉冲击力强。
  104. 中效传达(8-12分): 封面文字尚可,视觉元素有一定吸引力,但略显复杂。
  105. 低效传达(4-7分): 封面文字晦涩难懂,视觉元素杂乱无章。色彩暗淡,对比度弱。
  106. 无效传达(0-3分): 封面信息表达不清,无法理解视频内容。
  107. 四、情感共鸣与回忆杀 (满分20分):
  108. 高度共鸣(17-20分): 封面主题能够引发老年人对过去岁月的回忆,勾起他们对老朋友、老时光的思念,产生强烈的情感共鸣,引发对晚年生活的思考。例如同学情、怀旧主题等。
  109. 中度共鸣(11-16分): 封面主题有一定怀旧元素,能引发部分老年人的回忆,并进行一定程度的思考。
  110. 低度共鸣(6-10分): 封面主题怀旧元素较少,共鸣感不强。
  111. 无共鸣(0-5分): 封面主题与回忆无关。
  112. 五、正能量与精神寄托 (满分15分):
  113. 高度正能量(13-15分): 封面内容积极向上,能够给予老年人希望和力量,或者包含宗教、祈福等元素,满足他们的精神寄托。
  114. 中度正能量(8-12分): 封面内容有一定的积极意义,但不够突出。
  115. 低度正能量(4-7分): 封面内容较为平淡,缺乏精神寄托。
  116. 负能量 (0-3分): 封面内容消极悲观,或者与老年人的信仰不符。
  117. 六、节日/时事关联性 (满分10分):
  118. 高度关联(8-10分): 封面与节日、时事热点紧密相关,能激发老年人的分享欲和参与感。
  119. 中度关联(5-7分): 封面与节日或时事有一定关联,但并非核心。
  120. 低度关联(2-4分): 封面与节日或时事关联较弱。
  121. 无关联(0-1分): 封面与节日、时事无关。
  122. 七、传播动机 (满分10分):
  123. 强传播动机(8-10分): 封面内容能激发老年人强烈的情感,例如激动、感动、惊讶等,让他们想要分享给家人朋友,或者认为视频内容对他人有帮助,有分享的价值。
  124. 中等传播动机(5-7分): 封面内容有一定分享价值,但分享意愿不强烈。
  125. 低传播动机(2-4分): 封面内容平淡,缺乏分享动力。
  126. 无传播动机(0-1分): 封面内容无分享价值,或者会引起不适,降低传播意愿。
  127. 八、附加分(满分60分)
  128. 1.包含老年人为画面主体(0-5分)
  129. 2.有超过3人为画面主体(0-5分)
  130. 3.充斥画面的密集人群为画面主体(0-5分)
  131. 4.存在知名历史、近代人物(0-5分)
  132. 5.存在人物脸部、头部未完整出现在画面的情况(0-5分)
  133. 6.是不以人为主体的鲜花、美景、知名建筑或风景(0-5分)
  134. 7.是老照片、怀旧风格(0-5分)
  135. 8.是农村、军事、综艺演出、历史画面(0-5分)
  136. 9.有趣味、惊奇的形象或画面为主体(0-5分)
  137. 10.以大号文字或密集文字为主体并且不包含人物(0-5分)
  138. 11.是不包含人物的纯色画面(0-5分)
  139. 12.是模糊的或清晰度、像素较低的(0-5分)
  140. 总分评估:110-160分: 高度吸引老年人群体,有成为爆款的潜力。
  141. 80-109分: 具有一定吸引力,值得尝试。
  142. 65-79分: 吸引力一般,需要优化。
  143. 65分以下: 吸引力不足,不建议使用。
  144. 注意:输出只需要返回 'hh:mm:ss.xxx' 格式的时间, 无需返回任何其他东西
  145. """
  146. return extract_prompt
  147. @retry(**retry_desc)
  148. def get_video_cover(video_oss_path: str, time_millisecond_str: str) -> Optional[Dict]:
  149. """
  150. input video oss path and time millisecond
  151. output video cover image oss path
  152. """
  153. video_url = "https://rescdn.yishihui.com/" + video_oss_path
  154. url = "http://192.168.205.80:8080/ffmpeg/fetchKeyFrames"
  155. data = {"url": video_url, "timestamp": time_millisecond_str}
  156. headers = {
  157. "content-type": "application/json",
  158. }
  159. try:
  160. response = requests.post(url, headers=headers, json=data, timeout=60)
  161. response.raise_for_status()
  162. return response.json()
  163. except RequestException as e:
  164. print(f"API请求失败: {e}")
  165. except json.JSONDecodeError as e:
  166. print(f"响应解析失败: {e}")
  167. return None
  168. def normalize_time_str(time_string: str) -> str | None:
  169. # 预处理:替换中文冒号、去除特殊标记、清理空格
  170. time_str = re.sub(r":", ":", time_string) # 中文冒号转英文
  171. time_str = re.sub(r"\s*(\d+\.\s*)?\*+\s*", "", time_str) # 去除数字编号和**标记
  172. time_str = time_str.strip()
  173. # 组合式正则匹配(按优先级排序)
  174. patterns = [
  175. # hh:mm:ss.xxx
  176. (r"^(\d{2}):(\d{2}):(\d{2})\.(\d{3})$", None),
  177. # h:mm:ss.xxx
  178. (
  179. r"^(\d{1}):(\d{2}):(\d{2})\.(\d{3})$",
  180. lambda m: f"0{m[1]}:{m[2]}:{m[3]}.{m[4]}",
  181. ),
  182. # mm:ss.xxx
  183. (r"^(\d{2}):(\d{2})\.(\d{3})$", lambda m: f"00:{m[1]}:{m[2]}.{m[3]}"),
  184. # m:ss.xxx
  185. (r"^(\d{1}):(\d{2})\.(\d{3})$", lambda m: f"00:0{m[1]}:{m[2]}.{m[3]}"),
  186. ]
  187. for pattern, processor in patterns:
  188. if match := re.fullmatch(pattern, time_str):
  189. return processor(match) if processor else time_str
  190. # 特殊处理 dd:dd:ddd 格式(假设最后3位为毫秒)
  191. if m := re.fullmatch(r"(\d{2}:\d{2}):(\d{3})", time_str):
  192. return f"00:{m[1]}.{m[2]}"
  193. return None