video_stitching.py 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355
  1. # -*- coding: utf-8 -*-
  2. # @Time: 2023/12/26
  3. import datetime
  4. import random
  5. import os
  6. import sys
  7. import time
  8. import subprocess
  9. import resource
  10. import psutil
  11. import requests
  12. import urllib.parse
  13. sys.path.append(os.getcwd())
  14. from common import Feishu
  15. from common.aliyun_oss_uploading import Oss
  16. from common.common import Common
  17. from common.db import MysqlHelper
  18. from common.material import Material
  19. from moviepy.editor import VideoFileClip, concatenate_videoclips
  20. from moviepy import editor
  21. output_path = "./video_stitching/video/new_video.mp4"
  22. class VideoStitching():
  23. @classmethod
  24. def split_text(cls, text, max_length):
  25. words = text.split(' ')
  26. lines = []
  27. current_line = ''
  28. for word in words:
  29. if len(current_line) + len(word) <= max_length:
  30. current_line += word + ' '
  31. else:
  32. lines.append(current_line.strip())
  33. current_line = word + ' '
  34. lines.append(current_line.strip())
  35. result = ''.join(lines)
  36. result = result[:11] + '\n' + result[11:] # 在第10个字后面增加换行
  37. return result
  38. @classmethod
  39. def srt_to_seconds(cls,srt_time):
  40. hours, minutes, seconds = map(float, srt_time.replace(',', '.').split(':'))
  41. return hours * 3600 + minutes * 60 + seconds
  42. @classmethod
  43. def insert_videoAudio(cls, audio_url, i):
  44. for j in audio_url:
  45. insert_sql = f"""INSERT INTO video_audio (audio, video_id, account_id, oss_object_key) values ("{i}", "{j[0]}", "{j[1]}", "{j[2]}")"""
  46. MysqlHelper.update_values(
  47. sql=insert_sql,
  48. env="prod",
  49. machine="",
  50. )
  51. # 随机生成id
  52. @classmethod
  53. def random_id(cls):
  54. now = datetime.datetime.now()
  55. rand_num = random.randint(10000, 99999)
  56. id = "{}{}".format(now.strftime("%Y%m%d%H%M%S"), rand_num)
  57. return id
  58. @classmethod
  59. def get_account_id(cls):
  60. account_id = f"""select account_id from video_url group by account_id;"""
  61. account_id = MysqlHelper.get_values(account_id, "prod")
  62. return account_id
  63. @classmethod
  64. def get_url_list(cls, audio_id, account):
  65. url_list = f"""SELECT a.video_id,a.account_id,a.oss_object_key FROM video_url a WHERE NOT EXISTS (
  66. SELECT video_id
  67. FROM video_audio b
  68. WHERE a.video_id = b.video_id AND b.audio = "{audio_id}"
  69. ) AND a.account_id = {account} ;"""
  70. url_list = MysqlHelper.get_values(url_list, "prod")
  71. return url_list
  72. # 新生成视频上传到对应账号下
  73. @classmethod
  74. def insert_piaoquantv(cls, oss_object_key):
  75. code = 1
  76. list = ["66481136", "66481137", "66481140", "66481141", "66481142"]
  77. for item in list:
  78. title = Material.get_title()
  79. url = "https://vlogapi.piaoquantv.com/longvideoapi/crawler/video/send"
  80. payload = dict(pageSource='vlog-pages/post/post-video-post', videoPath=oss_object_key, width='720',
  81. height='1280', fileExtensions='mp4', viewStatus='1', title=title, careModelStatus='1',
  82. token='f04f58d6e664cbc9902660a1e8d20ce6cd7fdb0f', loginUid=item, versionCode='719',
  83. machineCode='weixin_openid_o0w175aZ4FJtqVsA1tcozJDJHdDU', appId='wx89e7eb06478361d7',
  84. clientTimestamp='1703337579331',
  85. machineInfo='{"sdkVersion":"3.2.5","brand":"iPhone","language":"zh_CN","model":"iPhone 12 Pro<iPhone13,3>","platform":"ios","system":"iOS 15.6.1","weChatVersion":"8.0.44","screenHeight":844,"screenWidth":390,"pixelRatio":3,"windowHeight":762,"windowWidth":390,"softVersion":"4.1.719"}',
  86. sessionId='1703337560040-27bfe208-a389-f476-db1d-840681e04b32',
  87. subSessionId='1703337569952-8f56d53c-b36d-760e-8abe-0b4a027cd5bd', senceType='1089',
  88. hotSenceType='1089', id='1050', channel='pq')
  89. payload['videoPath'] = oss_object_key
  90. payload['title'] = title
  91. data = urllib.parse.urlencode(payload)
  92. headers = {
  93. 'User-Agent': 'Mozilla/5.0 (iPhone; CPU iPhone OS 15_6_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148 MicroMessenger/8.0.44(0x18002c2d) NetType/WIFI Language/zh_CN',
  94. 'Accept-Encoding': 'gzip,compress,br,deflate',
  95. 'Referer': 'https://servicewechat.com/wx89e7eb06478361d7/726/page-frame.html',
  96. 'Content-Type': 'application/x-www-form-urlencoded',
  97. 'Cookie': 'JSESSIONID=A60D96E7A300A25EA05425B069C8B459'
  98. }
  99. response = requests.post(url, data=data, headers=headers)
  100. data = response.json()
  101. code = data["code"]
  102. if code == 0:
  103. return True
  104. else:
  105. return False
  106. def get_io_bps(cls):
  107. io_counters = psutil.Process().io_counters()
  108. read_bps = io_counters.read_bytes / 5 # 每秒读取的字节数
  109. write_bps = io_counters.write_bytes / 5 # 每秒写入的字节数
  110. Common.logger().info(f"当前读取速度:{read_bps} B/s")
  111. Common.logger().info(f"当前写入速度:{write_bps} B/s")
  112. # 视频拼接
  113. @classmethod
  114. def concatenate_videos(cls, videos, audio, srt):
  115. clips = []
  116. total_duration = 0
  117. included_videos = []
  118. # 设置最大可使用的内存限制(单位:字节)
  119. memory_limit = 6 * 1024 * 1024 * 1024 # 6GB
  120. resource.setrlimit(resource.RLIMIT_AS, (memory_limit, memory_limit))
  121. # 提取视频的音频
  122. Common.logger().info(f"开始提取视频的音频{audio}")
  123. video1 = VideoFileClip(audio)
  124. mp3 = video1.audio
  125. Common.logger().info(f"提取视频的音频成功")
  126. # 获取音频时长(以秒为单位)
  127. duration_limit = mp3.duration
  128. # 遍历每个视频并计算总时长
  129. for i, video in enumerate(videos):
  130. clip = VideoFileClip(video[3])
  131. clips.append(clip)
  132. total_duration += clip.duration
  133. if total_duration >= duration_limit:
  134. break
  135. # 如果总时长小于等于目标时长,则不做视频拼接
  136. if total_duration <= duration_limit:
  137. Common.logger().info(f"时长小于等于目标时长,不做视频拼接")
  138. return ""
  139. else:
  140. Common.logger().info(f"总时长大于目标时长")
  141. remaining_time = duration_limit
  142. final_clips = []
  143. for clip, video in zip(clips, videos):
  144. if remaining_time - clip.duration >= 0:
  145. final_clips.append(clip)
  146. included_videos.append(video)
  147. remaining_time -= clip.duration
  148. else:
  149. # 如果剩余时间不足以加入下一个视频,则截断当前视频并返回已包含的URL
  150. final_clips.append(clip.subclip(0, remaining_time))
  151. included_videos.append(video)
  152. break
  153. final_clip = concatenate_videoclips(final_clips)
  154. final_clip = final_clip.set_audio(mp3)
  155. # 统一设置视频分辨率
  156. final_width = 480
  157. final_height = 720
  158. final_clip = final_clip.resize((final_width, final_height))
  159. # 设置背景色
  160. color_clip = editor.ColorClip(size=(final_width, 120),
  161. color=(255, 255, 0)).set_duration(duration_limit)
  162. final_clip = editor.CompositeVideoClip([final_clip, color_clip.set_position(("center", final_height - 100))])
  163. # 处理SRT字幕文件
  164. subtitle_file = f"./video_stitching/video/{srt}.srt"
  165. Common.logger().info(f"处理SRT字幕文件")
  166. if os.path.isfile(subtitle_file):
  167. with open(subtitle_file, 'r') as file:
  168. cls.get_io_bps()
  169. subtitles = file.read().strip().split('\n\n')
  170. # 从SRT字幕文件中获取字幕
  171. subtitle_clips = []
  172. for subtitle in subtitles:
  173. # 按行分割字幕内容
  174. subtitle_lines = subtitle.strip().split('\n')
  175. # 提取时间轴信息和字幕文本
  176. if len(subtitle_lines) >= 3:
  177. times, text = subtitle_lines[1], '\n'.join(subtitle_lines[2:])
  178. start, end = map(cls.srt_to_seconds, times.split(' --> '))
  179. start = editor.cvsecs(start)
  180. end = editor.cvsecs(end)
  181. text = cls.split_text(text, 10)
  182. # /System/Library/Fonts/Hiragino Sans GB.ttc 本地字体
  183. Common.logger().info(f"字幕:{text}")
  184. sub = editor.TextClip(text, font="/usr/share/fonts/truetype/wqy/wqy-zenhei.ttc",
  185. fontsize=30, color="black").set_duration(end - start).set_start(
  186. start).set_position(
  187. ("center", final_height - 80)).set_opacity(0.8)
  188. subtitle_clips.append(sub)
  189. Common.logger().info(f"将字幕添加到视频上")
  190. # 将字幕添加到视频上
  191. video_with_subtitles = editor.CompositeVideoClip([final_clip] + subtitle_clips)
  192. else:
  193. text_clip = (
  194. editor.TextClip("分享、转发给群友", font="/usr/share/fonts/truetype/wqy/wqy-zenhei.ttc",
  195. fontsize=30, color="black").
  196. set_position(("center", final_height - 80)).
  197. set_duration(duration_limit).
  198. set_opacity(0.8)
  199. )
  200. # 把 `文本剪贴板` 贴在视频上
  201. video_with_subtitles = editor.CompositeVideoClip([final_clip, text_clip])
  202. # 生成视频
  203. cls.get_io_bps()
  204. video_with_subtitles.write_videofile(output_path, codec='libx264', fps=24)
  205. if os.path.isfile(output_path):
  206. Common.logger().info("视频生成成功!生成路径为:", output_path)
  207. return included_videos, video_with_subtitles
  208. else:
  209. Common.logger().info("视频生成失败,请检查代码和文件路径。")
  210. return "", video_with_subtitles
  211. @classmethod
  212. def get_audio_url(cls, i, cookie):
  213. url = f"https://admin.piaoquantv.com/manager/video/detail/{i}"
  214. payload = {}
  215. headers = {
  216. 'authority': 'admin.piaoquantv.com',
  217. 'accept': 'application/json, text/plain, */*',
  218. 'accept-language': 'zh-CN,zh;q=0.9',
  219. 'cache-control': 'no-cache',
  220. 'cookie': cookie,
  221. 'pragma': 'no-cache',
  222. 'referer': f'https://admin.piaoquantv.com/cms/post-detail/{i}/detail',
  223. 'sec-ch-ua': '"Not_A Brand";v="8", "Chromium";v="120", "Google Chrome";v="120"',
  224. 'sec-ch-ua-mobile': '?0',
  225. 'sec-ch-ua-platform': '"macOS"',
  226. 'sec-fetch-dest': 'empty',
  227. 'sec-fetch-mode': 'cors',
  228. 'sec-fetch-site': 'same-origin',
  229. 'user-agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36'
  230. }
  231. response = requests.request("GET", url, headers=headers, data=payload)
  232. data = response.json()
  233. try:
  234. code = data["code"]
  235. if code != 0:
  236. Common.logger().info(
  237. f"未登录,请更换cookie,{data}")
  238. Feishu.bot('recommend', '管理后台', '管理后台cookie失效,请及时更换~')
  239. # 如果返回空信息,则随机睡眠 600, 1200 秒
  240. time.sleep(random.randint(600, 1200))
  241. cls.video_cookie()
  242. audio_url = data["content"]["transedVideoPath"]
  243. print(audio_url)
  244. return audio_url
  245. except Exception as e:
  246. Common.logger().warning(f"获取音频视频链接失败:{e}\n")
  247. return ""
  248. @classmethod
  249. def video_stitching(cls, cookie):
  250. count = 0
  251. while True:
  252. # 获取音频
  253. audioid = Material.get_audio()
  254. # 获取已入库的用户id
  255. account_id = cls.get_account_id()
  256. audio_id = random.choice(audioid)
  257. account = random.choice(account_id)
  258. account = str(account).replace('(', '').replace(')', '').replace(',', '')
  259. Common.logger().info(f"获取用户ID:{account}")
  260. # 获取 未使用的视频链接
  261. url_list = cls.get_url_list(audio_id, account)
  262. # 获取音频url
  263. audio = cls.get_audio_url(audio_id, cookie)
  264. if audio == "":
  265. continue
  266. Common.logger().info(f"获取音频地址:{audio},获取用户id:{audio_id}")
  267. videos = [list(item) for item in url_list]
  268. videos = Oss.get_oss_url(videos)
  269. # 视频截取
  270. try:
  271. audio_url, video_with_subtitles = cls.concatenate_videos(videos, str(audio), audio_id)
  272. if len(audio_url) == 0:
  273. Common.logger().info(f"视频生成失败")
  274. continue
  275. # 随机生成视频id
  276. id = cls.random_id()
  277. Common.logger().info(f"生成视频id为:{id}")
  278. # 上传 oss
  279. oss_object_key = Oss.stitching_sync_upload_oss(output_path, id)
  280. status = oss_object_key.get("status")
  281. # 获取 oss 视频地址
  282. oss_object_key = oss_object_key.get("oss_object_key")
  283. Common.logger().info(f"新拼接视频,oss发送成功,oss地址:{oss_object_key}")
  284. if status == 200:
  285. time.sleep(10)
  286. # 发送成功 已使用视频存入数据库
  287. cls.insert_videoAudio(audio_url, audio)
  288. Common.logger().info(f"发送成功 已使用视频存入数据库完成")
  289. if os.path.isfile(output_path):
  290. os.remove(output_path)
  291. Common.logger().info(f"文件删除成功{output_path}")
  292. else:
  293. Common.logger().info(f"文件不存在{output_path}")
  294. piaoquantv = cls.insert_piaoquantv(oss_object_key)
  295. if piaoquantv:
  296. time.sleep(120)
  297. count += 1
  298. Common.logger().info(f"视频添加到对应用户成功")
  299. if count >= 5:
  300. break
  301. # 释放视频对象
  302. video_with_subtitles.close()
  303. except Exception as e:
  304. Common.logger().warning(f"新拼接视频发送oss失败:{e}\n")
  305. continue
  306. if count >= 5:
  307. break
  308. @classmethod
  309. def video_cookie(cls):
  310. # 获取后台cookie
  311. cookie = Material.get_houtai_cookie()
  312. cls.video_stitching(cookie)
  313. if __name__ == '__main__':
  314. VideoStitching.video_stitching()