videoAddTrigger.py 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320
  1. # 读取视频分析报告1_拆分钩子.xlsx
  2. # 提取视频链接,提取hook和time
  3. # 下载视频,
  4. # 使用ffmpeg将hook文案加入到视频内
  5. # 保存处理后的视频至 trigger_video 文件夹内
  6. import pandas as pd
  7. import os
  8. import requests
  9. import subprocess
  10. from datetime import datetime
  11. import time
  12. from pathlib import Path
  13. import shutil
  14. import oss2
  15. from oss2.credentials import EnvironmentVariableCredentialsProvider
  16. def download_video(url, save_path):
  17. """下载视频文件,支持断点续传"""
  18. try:
  19. # 创建临时文件
  20. temp_path = save_path + '.tmp'
  21. # 获取已下载的文件大小
  22. initial_pos = 0
  23. if os.path.exists(temp_path):
  24. initial_pos = os.path.getsize(temp_path)
  25. headers = {'Range': f'bytes={initial_pos}-'} if initial_pos > 0 else {}
  26. # 发送请求
  27. response = requests.get(url, headers=headers, stream=True)
  28. response.raise_for_status()
  29. # 获取文件总大小
  30. total_size = int(response.headers.get('content-length', 0)) + initial_pos
  31. # 写入文件
  32. mode = 'ab' if initial_pos > 0 else 'wb'
  33. with open(temp_path, mode) as f:
  34. for chunk in response.iter_content(chunk_size=8192):
  35. if chunk:
  36. f.write(chunk)
  37. # 下载完成后重命名
  38. shutil.move(temp_path, save_path)
  39. return True
  40. except Exception as e:
  41. print(f"下载视频失败: {str(e)}")
  42. if os.path.exists(temp_path):
  43. os.remove(temp_path)
  44. return False
  45. def add_text_to_video(input_video, output_video, text, start_time, end_time):
  46. """使用ffmpeg添加文字到视频中,在指定的时间段内显示
  47. 文字样式:
  48. - 红色背景
  49. - 白色18像素加粗字体
  50. - 自动换行(每行最多20个字符)
  51. - 位置在底部50像素处
  52. """
  53. try:
  54. # 处理文本换行(每行最多20个字符)
  55. wrapped_text = text.replace('\\n', '\n') # 保留原有的换行符
  56. if len(text) > 20 and '\\n' not in text:
  57. # 在合适的位置添加换行符
  58. words = text.split()
  59. lines = []
  60. current_line = []
  61. current_length = 0
  62. for word in words:
  63. if current_length + len(word) + 1 <= 20: # +1 for space
  64. current_line.append(word)
  65. current_length += len(word) + 1
  66. else:
  67. lines.append(' '.join(current_line))
  68. current_line = [word]
  69. current_length = len(word)
  70. if current_line:
  71. lines.append(' '.join(current_line))
  72. wrapped_text = '\\n'.join(lines)
  73. # 构建ffmpeg命令
  74. # 使用drawtext滤镜添加文字,设置字体、颜色、位置等
  75. cmd = [
  76. 'ffmpeg', '-y',
  77. '-i', input_video,
  78. '-vf', f"drawtext=text='{wrapped_text}'"
  79. f":fontsize=18" # 字体大小18像素
  80. f":fontcolor=white" # 白色字体
  81. f":fontfile=/System/Library/Fonts/PingFang.ttc" # 使用系统字体
  82. f":fontweight=bold" # 字体加粗
  83. f":box=1" # 启用背景框
  84. f":boxcolor=red@0.8" # 红色背景,透明度0.8
  85. f":boxborderw=5" # 背景框边框宽度
  86. f":x=(w-text_w)/2" # 水平居中
  87. f":y=h-th-50" # 距离底部50像素
  88. f":line_spacing=10" # 行间距
  89. f":enable='between(t,{start_time},{end_time})'", # 显示时间段
  90. '-c:a', 'copy', # 保持音频不变
  91. output_video
  92. ]
  93. # 执行命令
  94. process = subprocess.Popen(
  95. cmd,
  96. stdout=subprocess.PIPE,
  97. stderr=subprocess.PIPE,
  98. universal_newlines=True
  99. )
  100. # 等待命令执行完成
  101. stdout, stderr = process.communicate()
  102. if process.returncode != 0:
  103. print(f"添加文字失败: {stderr}")
  104. return False
  105. return True
  106. except Exception as e:
  107. print(f"处理视频时出错: {str(e)}")
  108. return False
  109. def parse_time(time_str):
  110. """解析时间字符串,支持以下格式:
  111. 1. HH:MM:SS-HH:MM:SS (时间范围)
  112. 2. HH:MM:SS (单个时间点,显示5秒)
  113. 3. "视频结束" (在视频结束前5秒显示)
  114. """
  115. try:
  116. if time_str == "视频结束":
  117. return None, None # 特殊标记,需要后续处理
  118. if '-' in time_str:
  119. # 处理时间范围
  120. start_str, end_str = time_str.split('-')
  121. # 解析开始时间
  122. h1, m1, s1 = map(int, start_str.split(':'))
  123. start_time = h1 * 3600 + m1 * 60 + s1
  124. # 解析结束时间
  125. h2, m2, s2 = map(int, end_str.split(':'))
  126. end_time = h2 * 3600 + m2 * 60 + s2
  127. return start_time, end_time
  128. else:
  129. # 处理单个时间点
  130. h, m, s = map(int, time_str.split(':'))
  131. start_time = h * 3600 + m * 60 + s
  132. return start_time, start_time + 5 # 默认显示5秒
  133. except Exception as e:
  134. print(f"时间格式解析失败: {time_str}, 错误: {str(e)}")
  135. return None, None
  136. def process_videos():
  137. """处理所有视频数据"""
  138. # 创建输出目录
  139. output_dir = Path("trigger_video")
  140. output_dir.mkdir(exist_ok=True)
  141. # 创建临时目录
  142. temp_dir = Path("temp_videos")
  143. temp_dir.mkdir(exist_ok=True)
  144. try:
  145. # 读取Excel文件
  146. print("开始读取Excel文件...")
  147. df = pd.read_excel("视频分析报告1_拆分钩子.xlsx")
  148. total_rows = len(df)
  149. print(f"共读取到 {total_rows} 行数据")
  150. # 处理每一行
  151. for idx, row in df.iterrows():
  152. try:
  153. print(f"\n{'='*50}")
  154. print(f"开始处理第 {idx+1}/{total_rows} 行")
  155. print(f"{'='*50}")
  156. video_url = row.iloc[3] # 视频URL在第4列
  157. if pd.isna(video_url):
  158. print(f"第 {idx+1} 行没有视频URL,跳过")
  159. continue
  160. print(f"视频URL: {video_url}")
  161. # 获取hook信息
  162. hooks = row.iloc[11].split('\n') # hook在第12列
  163. times = row.iloc[9].split('\n') # time在第10列
  164. print(f"钩子数量: {len(hooks)}")
  165. print(f"时间点数量: {len(times)}")
  166. if not hooks or not times or len(hooks) != len(times):
  167. print(f"第 {idx+1} 行hook或time数据不完整,跳过")
  168. continue
  169. # 生成输出文件名
  170. video_id = f"video_{idx+1}"
  171. temp_video = temp_dir / f"{video_id}.mp4"
  172. output_video = output_dir / f"{video_id}_with_hooks.mp4"
  173. # 如果输出文件已存在,跳过处理
  174. if output_video.exists():
  175. print(f"视频 {output_video} 已存在,跳过处理")
  176. continue
  177. # 下载视频
  178. print(f"\n开始下载视频...")
  179. if not download_video(video_url, str(temp_video)):
  180. print(f"第 {idx+1} 行视频下载失败,跳过")
  181. continue
  182. # 获取视频总时长
  183. print("\n获取视频时长...")
  184. cmd = ['ffprobe', '-v', 'error', '-show_entries', 'format=duration',
  185. '-of', 'default=noprint_wrappers=1:nokey=1', str(temp_video)]
  186. video_duration = float(subprocess.check_output(cmd).decode().strip())
  187. print(f"视频总时长: {video_duration:.2f}秒")
  188. # 处理每个hook
  189. current_video = temp_video
  190. for i, (hook, time_str) in enumerate(zip(hooks, times)):
  191. if not hook.strip() or not time_str.strip():
  192. print(f"\n跳过空的hook或时间点")
  193. continue
  194. print(f"\n处理第 {i+1}/{len(hooks)} 个钩子:")
  195. print(f"钩子内容: {hook}")
  196. print(f"时间点: {time_str}")
  197. # 解析时间
  198. start_time, end_time = parse_time(time_str)
  199. if start_time is None:
  200. if time_str == "视频结束":
  201. # 在视频结束前5秒显示
  202. start_time = video_duration - 5
  203. end_time = video_duration
  204. print(f"设置为视频结束前5秒显示")
  205. else:
  206. print(f"无效的时间格式: {time_str},跳过")
  207. continue
  208. print(f"开始时间: {start_time:.2f}秒")
  209. print(f"结束时间: {end_time:.2f}秒")
  210. # 确保时间在视频范围内
  211. if start_time >= video_duration:
  212. print(f"开始时间超出视频时长,跳过")
  213. continue
  214. end_time = min(end_time, video_duration)
  215. # 添加文字到视频
  216. temp_output = temp_dir / f"{video_id}_temp_{i}.mp4"
  217. print(f"正在添加文字到视频...")
  218. if not add_text_to_video(str(current_video), str(temp_output), hook, start_time, end_time):
  219. print("添加文字失败,跳过")
  220. continue
  221. # 更新当前视频路径
  222. if current_video != temp_video:
  223. os.remove(current_video)
  224. current_video = temp_output
  225. print("文字添加成功")
  226. # 移动最终视频到输出目录
  227. print(f"\n处理完成,保存最终视频...")
  228. shutil.move(str(current_video), str(output_video))
  229. print(f"视频已保存到: {output_video}")
  230. oss_url = upload_to_oss(f"{video_id}_with_hooks.mp4")
  231. print(f"上传成功: {oss_url}")
  232. # 将oss_url写入excel 12列
  233. df.loc[idx, 12] = oss_url
  234. df.to_excel("视频分析报告1_拆分钩子_with_oss_url.xlsx", index=False)
  235. except Exception as e:
  236. print(f"处理第 {idx+1} 行时出错: {str(e)}")
  237. continue
  238. finally:
  239. # 清理临时文件
  240. if temp_dir.exists():
  241. print("\n清理临时文件...")
  242. shutil.rmtree(temp_dir)
  243. print("\n所有视频处理完成!")
  244. # 处理完成之后上传至阿里云oss
  245. def upload_to_oss(object_name):
  246. auth = oss2.ProviderAuthV4(EnvironmentVariableCredentialsProvider())
  247. # 填写Bucket所在地域对应的Endpoint。以华东1(杭州)为例,Endpoint填写为https://oss-cn-hangzhou.aliyuncs.com。
  248. endpoint = "https://oss-cn-hangzhou.aliyuncs.com"
  249. # 填写Endpoint对应的Region信息,例如cn-hangzhou。注意,v4签名下,必须填写该参数
  250. region = "cn-hangzhou"
  251. # 填写Bucket名称,例如examplebucket。
  252. bucketName = "art-weapp"
  253. # 创建Bucket实例,指定存储空间的名称和Region信息。
  254. bucket = oss2.Bucket(auth, endpoint, bucketName, region=region)
  255. # 本地文件的完整路径
  256. local_file_path = '/Users/jihuaqiang/piaoquan/video-comprehension/' + object_name
  257. # 填写Object完整路径,完整路径中不能包含Bucket名称。例如exampleobject.txt。
  258. objectName = 'ai-trigger-demo/' + object_name
  259. # 使用put_object_from_file方法将本地文件上传至OSS
  260. bucket.put_object_from_file(objectName, local_file_path)
  261. print(f"上传成功: https://art-weapp.oss-cn-hangzhou.aliyuncs.com/{objectName}")
  262. return f"https://art-weapp.oss-cn-hangzhou.aliyuncs.com/{objectName}"
  263. if __name__ == "__main__":
  264. # process_videos()
  265. #
  266. upload_to_oss("57463792VYj3UHnLFS6lAufeAy20250512191000803651096-1LD.mp4")