agc_video.py 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498
  1. import configparser
  2. import glob
  3. import os
  4. import random
  5. import re
  6. import subprocess
  7. import sys
  8. import time
  9. import urllib.parse
  10. import json
  11. import requests
  12. from datetime import datetime, timedelta
  13. from urllib.parse import urlencode
  14. from common.sql_help import sqlHelp
  15. sys.path.append(os.getcwd())
  16. from common.db import MysqlHelper
  17. from common.material import Material
  18. from common import Common, Oss, Feishu, PQ
  19. from common.srt import SRT
  20. config = configparser.ConfigParser()
  21. config.read('./config.ini') # 替换为您的配置文件路径
  22. class AGC():
  23. """清除文件下所有mp4文件"""
  24. @classmethod
  25. def clear_mp4_files(cls, folder_path):
  26. # 获取文件夹中所有扩展名为 '.mp4' 的文件路径列表
  27. mp4_files = glob.glob(os.path.join(folder_path, '*.mp4'))
  28. if not mp4_files:
  29. return
  30. # 遍历并删除所有 .mp4 文件
  31. for mp4_file in mp4_files:
  32. os.remove(mp4_file)
  33. print(f"文件夹 '{folder_path}' 中的所有 .mp4 文件已清空。")
  34. """
  35. 站外视频拼接
  36. """
  37. @classmethod
  38. def zw_concatenate_videos(cls, videos, audio_duration, audio_video, platform, s_path, v_path, mark, v_oss_path):
  39. video_files = cls.concat_videos_with_subtitles(videos, audio_duration, platform, mark)
  40. Common.logger("video").info(f"{mark}的{platform}视频文件:{video_files}")
  41. if video_files == "":
  42. return ""
  43. print(f"{mark}的{platform}:开始拼接视频喽~~~")
  44. Common.logger("video").info(f"{mark}的{platform}:开始拼接视频喽~~~")
  45. if os.path.exists(s_path):
  46. # subtitle_cmd = f"subtitles={s_path}:force_style='Fontsize=11,Fontname=Hiragino Sans GB,Outline=0,PrimaryColour=&H000000,SecondaryColour=&H000000'"
  47. subtitle_cmd = f"subtitles={s_path}:force_style='Fontsize=12,Fontname=wqy-zenhei,Bold=1,Outline=0,PrimaryColour=&H000000,SecondaryColour=&H000000'"
  48. else:
  49. start_time = cls.seconds_to_srt_time(0)
  50. end_time = cls.seconds_to_srt_time(audio_duration)
  51. with open(s_path, 'w') as f:
  52. f.write(f"1\n{start_time} --> {end_time}\n分享、转发给群友\n")
  53. # subtitle_cmd = "drawtext=text='分享、转发给群友':fontsize=28:fontcolor=black:x=(w-text_w)/2:y=h-text_h-15"
  54. subtitle_cmd = f"subtitles={s_path}:force_style='Fontsize=12,Fontname=wqy-zenhei,Bold=1,Outline=0,PrimaryColour=&H000000,SecondaryColour=&H000000'"
  55. # 背景色参数
  56. background_cmd = "drawbox=y=ih-65:color=yellow@1.0:width=iw:height=0:t=fill"
  57. VIDEO_COUNTER = 0
  58. FF_INPUT = ""
  59. FF_SCALE = ""
  60. FF_FILTER = ""
  61. ffmpeg_cmd = ["ffmpeg"]
  62. for videos in video_files:
  63. Common.logger("video").info(f"{mark}的{platform}视频:{videos[3]}")
  64. # 添加输入文件
  65. FF_INPUT += f" -i {videos[3]}"
  66. # 为每个视频文件统一长宽,并设置SAR(采样宽高比)
  67. FF_SCALE += f"[{VIDEO_COUNTER}:v]scale=320x480,setsar=1[v{VIDEO_COUNTER}];"
  68. # 为每个视频文件创建一个输入流,并添加到-filter_complex参数中
  69. FF_FILTER += f"[v{VIDEO_COUNTER}][{VIDEO_COUNTER}:a]"
  70. # 增加视频计数器
  71. VIDEO_COUNTER += 1
  72. # 构建最终的FFmpeg命令
  73. ffmpeg_cmd.extend(FF_INPUT.split())
  74. ffmpeg_cmd.extend(["-filter_complex", f"{FF_SCALE}{FF_FILTER}concat=n={VIDEO_COUNTER}:v=1:a=1[v][a]",
  75. "-map", "[v]", "-map", "[a]", v_path])
  76. # 多线程数
  77. num_threads = 4
  78. # 构建 FFmpeg 命令,生成视频
  79. ffmpeg_cmd_oss = [
  80. "ffmpeg",
  81. "-i", v_path, # 视频文件列表
  82. "-i", audio_video, # 音频文件
  83. "-c:v", "libx264", # 复制视频流
  84. "-c:a", "aac", # 编码音频流为AAC
  85. "-threads", str(num_threads),
  86. "-vf", f"{background_cmd},{subtitle_cmd}", # 添加背景色和字幕
  87. "-t", str(int(audio_duration)), # 保持与音频时长一致
  88. "-map", "0:v:0", # 映射第一个输入的视频流
  89. "-map", "1:a:0", # 映射第二个输入的音频流
  90. "-y", # 覆盖输出文件
  91. v_oss_path
  92. ]
  93. try:
  94. subprocess.run(ffmpeg_cmd)
  95. if os.path.isfile(v_path):
  96. subprocess.run(ffmpeg_cmd_oss)
  97. print("视频处理完成!")
  98. except subprocess.CalledProcessError as e:
  99. print(f"视频处理失败:{e}")
  100. print(f"{mark}的{platform}:视频拼接成功啦~~~")
  101. Common.logger("video").info(f"{mark}的{platform}:视频拼接成功啦~~~")
  102. return video_files
  103. """视频秒数转换"""
  104. @classmethod
  105. def seconds_to_srt_time(cls, seconds):
  106. hours = int(seconds // 3600)
  107. minutes = int((seconds % 3600) // 60)
  108. seconds = seconds % 60
  109. milliseconds = int((seconds - int(seconds)) * 1000)
  110. return f"{hours:02d}:{minutes:02d}:{int(seconds):02d},{milliseconds:03d}"
  111. """
  112. 获取视频文件的时长(秒)
  113. """
  114. @classmethod
  115. def get_video_duration(cls, video_file):
  116. result = subprocess.run(
  117. ["ffprobe", "-v", "error", "-show_entries", "format=duration",
  118. "-of", "default=noprint_wrappers=1:nokey=1", video_file],
  119. capture_output=True, text=True)
  120. return float(result.stdout)
  121. """计算需要拼接的视频"""
  122. @classmethod
  123. def concat_videos_with_subtitles(cls, videos, audio_duration, platform, mark):
  124. # 计算视频文件列表总时长
  125. if platform == "爆款" or platform == "跟随":
  126. total_video_duration = sum(cls.get_video_duration(video_file) for video_file in videos)
  127. else:
  128. total_video_duration = sum(cls.get_video_duration(video_file[3]) for video_file in videos)
  129. if platform == "爆款" or platform == "跟随":
  130. # 视频时长大于音频时长
  131. if total_video_duration > audio_duration:
  132. return videos
  133. # 计算音频秒数与视频秒数的比率,然后加一得到需要的视频数量
  134. video_audio_ratio = audio_duration / total_video_duration
  135. videos_needed = int(video_audio_ratio) + 2
  136. trimmed_video_list = videos * videos_needed
  137. return trimmed_video_list
  138. else:
  139. # 如果视频总时长小于音频时长,则不做拼接
  140. if total_video_duration < audio_duration:
  141. Common.logger("video").info(f"{mark}的{platform}渠道时长小于等于目标时长,不做视频拼接")
  142. return ""
  143. # 如果视频总时长大于音频时长,则截断视频
  144. trimmed_video_list = []
  145. remaining_duration = audio_duration
  146. for video_file in videos:
  147. video_duration = cls.get_video_duration(video_file[3])
  148. if video_duration <= remaining_duration:
  149. # 如果视频时长小于或等于剩余时长,则将整个视频添加到列表中
  150. trimmed_video_list.append(video_file)
  151. remaining_duration -= video_duration
  152. else:
  153. trimmed_video_list.append(video_file)
  154. break
  155. return trimmed_video_list
  156. """
  157. text文件没有则创建目录
  158. """
  159. @classmethod
  160. def bk_text_folders(cls, mark):
  161. oss_id = cls.random_id()
  162. v_text_url = config['PATHS']['VIDEO_PATH'] + mark + "/text/"
  163. if not os.path.exists(v_text_url):
  164. os.makedirs(v_text_url)
  165. # srt 文件地址
  166. text_path = v_text_url + mark + f"{str(oss_id)}.text"
  167. return text_path
  168. """
  169. 站内视频拼接
  170. """
  171. @classmethod
  172. def zn_concatenate_videos(cls, videos, audio_duration, audio_video, platform, s_path, mark, v_oss_path):
  173. text_ptah = cls.bk_text_folders(mark)
  174. video_files = cls.concat_videos_with_subtitles(videos, audio_duration, platform, mark)
  175. with open(text_ptah, 'w') as f:
  176. for file in video_files:
  177. f.write(f"file '{file}'\n")
  178. Common.logger("video").info(f"{mark}的{platform}视频文件:{video_files}")
  179. if video_files == "":
  180. return ""
  181. print(f"{mark}的{platform}:开始拼接视频喽~~~")
  182. Common.logger("video").info(f"{mark}的{platform}:开始拼接视频喽~~~")
  183. if os.path.exists(s_path):
  184. # subtitle_cmd = f"subtitles={s_path}:force_style='Fontsize=11,Fontname=Hiragino Sans GB,Outline=0,PrimaryColour=&H000000,SecondaryColour=&H000000'"
  185. subtitle_cmd = f"subtitles={s_path}:force_style='Fontsize=12,Fontname=wqy-zenhei,Bold=1,Outline=0,PrimaryColour=&H000000,SecondaryColour=&H000000'"
  186. else:
  187. start_time = cls.seconds_to_srt_time(0)
  188. end_time = cls.seconds_to_srt_time(audio_duration)
  189. with open(s_path, 'w') as f:
  190. f.write(f"1\n{start_time} --> {end_time}\n分享、转发给群友\n")
  191. # subtitle_cmd = "drawtext=text='分享、转发给群友':fontsize=28:fontcolor=black:x=(w-text_w)/2:y=h-text_h-15"
  192. subtitle_cmd = f"subtitles={s_path}:force_style='Fontsize=12,Fontname=wqy-zenhei,Bold=1,Outline=0,PrimaryColour=&H000000,SecondaryColour=&H000000'"
  193. # 背景色参数
  194. background_cmd = "drawbox=y=ih-65:color=yellow@1.0:width=iw:height=0:t=fill"
  195. # 多线程数
  196. num_threads = 4
  197. # 构建 FFmpeg 命令,生成视频
  198. ffmpeg_cmd_oss = [
  199. "ffmpeg",
  200. "-f", "concat",
  201. "-safe", "0",
  202. "-i", f"{text_ptah}", # 视频文件列表
  203. "-i", audio_video, # 音频文件
  204. "-c:v", "libx264",
  205. "-c:a", "aac",
  206. "-threads", str(num_threads),
  207. "-vf", f"scale=320x480,{background_cmd},{subtitle_cmd}", # 添加背景色和字幕
  208. "-t", str(int(audio_duration)), # 保持与音频时长一致
  209. "-map", "0:v:0", # 映射第一个输入的视频流
  210. "-map", "1:a:0", # 映射第二个输入的音频流
  211. "-y", # 覆盖输出文件
  212. v_oss_path
  213. ]
  214. try:
  215. subprocess.run(ffmpeg_cmd_oss)
  216. print("视频处理完成!")
  217. if os.path.isfile(text_ptah):
  218. os.remove(text_ptah)
  219. except subprocess.CalledProcessError as e:
  220. print(f"视频处理失败:{e}")
  221. print(f"{mark}:视频拼接成功啦~~~")
  222. Common.logger("video").info(f"{mark}:视频拼接成功啦~~~")
  223. return v_oss_path
  224. """
  225. 获取视频时长
  226. """
  227. @classmethod
  228. def get_audio_duration(cls, video_url):
  229. ffprobe_cmd = [
  230. "ffprobe",
  231. "-i", video_url,
  232. "-show_entries", "format=duration",
  233. "-v", "quiet",
  234. "-of", "csv=p=0"
  235. ]
  236. output = subprocess.check_output(ffprobe_cmd).decode("utf-8").strip()
  237. return float(output)
  238. """
  239. 创建临时字幕
  240. """
  241. @classmethod
  242. def create_subtitle_file(cls, srt, s_path):
  243. with open(s_path, 'w') as f:
  244. f.write(srt)
  245. """
  246. 随机生成id
  247. """
  248. @classmethod
  249. def random_id(cls):
  250. now = datetime.now()
  251. rand_num = random.randint(10000, 99999)
  252. oss_id = "{}{}".format(now.strftime("%Y%m%d%H%M%S"), rand_num)
  253. return oss_id
  254. """
  255. 文件没有则创建目录
  256. """
  257. @classmethod
  258. def create_folders(cls, mark):
  259. oss_id = cls.random_id()
  260. video_path_url = config['PATHS']['VIDEO_PATH'] + mark + "/"
  261. # srt 目录
  262. s_path_url = config['PATHS']['VIDEO_PATH'] + mark + "/srt/"
  263. # oss 目录
  264. v_path_url = config['PATHS']['VIDEO_PATH'] + mark + "/oss/"
  265. if not os.path.exists(video_path_url):
  266. os.makedirs(video_path_url)
  267. if not os.path.exists(s_path_url):
  268. os.makedirs(s_path_url)
  269. if not os.path.exists(v_path_url):
  270. os.makedirs(v_path_url)
  271. # srt 文件地址
  272. s_path = s_path_url + mark + f"{str(oss_id)}.srt"
  273. # 最终生成视频地址
  274. v_path = v_path_url + mark + f"{str(oss_id)}.mp4"
  275. v_oss_path = v_path_url + mark + f"{str(oss_id)}oss.mp4"
  276. return s_path, v_path, video_path_url, v_oss_path
  277. """
  278. 获取未使用的数据
  279. """
  280. @classmethod
  281. def get_unique_uid_data(cls, data, count):
  282. unique_data_dict = {item['uid']: item for item in data}
  283. unique_data = list(unique_data_dict.values())
  284. if count > len(unique_data):
  285. return unique_data
  286. else:
  287. selected_items = []
  288. selected_uids = set()
  289. while len(selected_items) < count:
  290. # 随机选择一个元素
  291. item = random.choice(unique_data)
  292. uid = item['uid']
  293. if uid not in selected_uids:
  294. # 如果该uid还未被选择过,则将该元素添加到选中项列表中,并记录已选择的uid
  295. selected_items.append(item)
  296. selected_uids.add(uid)
  297. return selected_items
  298. """
  299. 任务处理
  300. """
  301. @classmethod
  302. def video(cls, data, platform):
  303. mark_name = data['mark_name'] # 负责人
  304. if platform == "爆款":
  305. pq_ids = data["pq_id"]
  306. pq_ids_list = pq_ids.split(',') # 账号ID
  307. mark = data["mark"] # 标示
  308. feishu_id = data["feishu_id"] # 飞书文档ID
  309. video_call = data["video_call"] # 脚本sheet
  310. list_data = Material.get_allbk_data(feishu_id, video_call)
  311. if len(list_data) == 0:
  312. Feishu.bot('recommend', 'AGC完成通知', f'爆款任务数为0,不做拼接', mark, mark_name)
  313. return mark
  314. elif platform == "常规":
  315. pq_ids = data["pq_id"]# 账号ID
  316. pq_ids_list = pq_ids.split(',')
  317. mark = data["mark"]
  318. feishu_id = data["feishu_id"] # 飞书文档ID
  319. video_call = data["video_call"] # 脚本sheet
  320. video_count = data["video_count"]
  321. if int(video_count) == 0:
  322. Feishu.bot('recommend', 'AGC完成通知', f'常规任务数为{video_count},不做拼接', mark, mark_name)
  323. return mark
  324. data_list, videos_mark = Material.get_all_data(feishu_id, video_call, mark)
  325. list_data = cls.get_unique_uid_data(data_list, int(video_count))
  326. elif platform == "跟随":
  327. pq_ids = data["pq_id"]
  328. pq_ids_list = pq_ids.split(',') # 账号ID
  329. mark = data["mark"] # 标示
  330. feishu_id = data["feishu_id"] # 飞书文档ID
  331. video_call = data["video_call"]
  332. video_count = data["video_count"]
  333. if int(video_count) == 0:
  334. Feishu.bot('recommend', 'AGC完成通知', f'跟随任务数为{video_count},不做拼接', mark, mark_name)
  335. return mark
  336. data_list, videos = Material.get_all_data(feishu_id, video_call, mark)
  337. list_data = cls.get_unique_uid_data(data_list, int(video_count))
  338. s_path, v_path, video_path_url, v_oss_path = cls.create_folders(mark)
  339. count = 0
  340. for d_list in list_data:
  341. try:
  342. uid = d_list['uid'] # 音频id
  343. srt = d_list['text'] # srt
  344. cover = d_list['cover']
  345. audio_title = d_list['title']
  346. if srt:
  347. # 创建临时字幕文件
  348. cls.create_subtitle_file(srt, s_path)
  349. Common.logger("bk_video").info(f"S{mark} 文件目录创建成功")
  350. else:
  351. srt_new = SRT.getSrt(int(uid))
  352. if srt_new:
  353. current_time = datetime.now()
  354. formatted_time = current_time.strftime("%Y-%m-%d %H:%M:%S")
  355. values = [[mark, str(uid), srt_new, formatted_time]]
  356. random_wait_time = random.uniform(0.5, 2.5)
  357. time.sleep(random_wait_time)
  358. Feishu.insert_columns("IbVVsKCpbhxhSJtwYOUc8S1jnWb", "jd9qD9", "ROWS", 1, 2)
  359. time.sleep(random_wait_time)
  360. Feishu.update_values("IbVVsKCpbhxhSJtwYOUc8S1jnWb", "jd9qD9", "A2:Z2", values)
  361. # 创建临时字幕文件
  362. cls.create_subtitle_file(srt_new, s_path)
  363. Common.logger("video").info(f"S{mark}的{platform}渠道RT 文件目录创建成功")
  364. # 获取音频
  365. audio_video = PQ.get_audio_url(uid)
  366. Common.logger("video").info(f"{mark}的{platform}渠道获音频成功")
  367. audio_duration = cls.get_audio_duration(audio_video)
  368. Common.logger("video").info(f"{mark}的{platform}渠道获取需要拼接的音频秒数为:{audio_duration}")
  369. if platform != "常规":
  370. if platform == "爆款":
  371. videos = d_list['video']
  372. if ',' in videos:
  373. videos = str(videos).split(',')
  374. else:
  375. videos = [str(videos)]
  376. video_id = random.choice(videos)
  377. video_url = PQ.get_audio_url(video_id)
  378. download_video = Oss.download_url(video_url, video_path_url, str(video_id))
  379. if download_video:
  380. video_files = cls.zn_concatenate_videos(download_video, audio_duration, audio_video, platform,
  381. s_path, mark, v_oss_path)
  382. if os.path.isfile(v_oss_path):
  383. Common.logger("video").info(f"{mark}的{platform}渠道新视频生成成功")
  384. else:
  385. Common.logger("video").info(f"{mark}的{platform}渠道新视频生成失败")
  386. continue
  387. else:
  388. chnnel_count = int(len(list_data)/2)
  389. if chnnel_count >= count:
  390. channel = "kuaishou"
  391. else:
  392. channel = "douyin"
  393. user_id = sqlHelp.get_user_id(channel, mark)
  394. url_list, user = sqlHelp.get_url_list(user_id, mark, "35")
  395. videos = [list(item) for item in url_list]
  396. videos = Oss.get_oss_url(videos, video_path_url)
  397. video_files = cls.zw_concatenate_videos(videos, audio_duration, audio_video, platform, s_path, v_path,
  398. mark, v_oss_path)
  399. if video_files == "":
  400. Common.logger("video").info(f"{mark}的{platform}渠道使用拼接视频为空")
  401. continue
  402. if os.path.isfile(v_oss_path):
  403. Common.logger("video").info(f"{mark}的{platform}渠道新视频生成成功")
  404. else:
  405. Common.logger("video").info(f"{mark}的{platform}渠道新视频生成失败")
  406. continue
  407. # 随机生成视频oss_id
  408. oss_id = cls.random_id()
  409. Common.logger("video").info(f"{mark}的{platform}渠道上传到 OSS 生成视频id为:{oss_id}")
  410. oss_object_key = Oss.stitching_sync_upload_oss(v_oss_path, oss_id)
  411. status = oss_object_key.get("status")
  412. if status == 200:
  413. # 获取 oss 视频地址
  414. oss_object_key = oss_object_key.get("oss_object_key")
  415. Common.logger("video").info(f"{mark}的{platform}渠道拼接视频发送成功,OSS 地址:{oss_object_key}")
  416. time.sleep(10)
  417. if platform == "常规":
  418. # 已使用视频存入数据库
  419. Common.logger("video").info(f"{mark}的{platform}渠道开始已使用视频存入数据库")
  420. sqlHelp.insert_videoAudio(video_files, uid, platform, mark)
  421. Common.logger("video").info(f"{mark}的{platform}渠道完成已使用视频存入数据库")
  422. Common.logger("video").info(f"{mark}的{platform}渠道开始视频添加到对应用户")
  423. new_video_id, title = PQ.insert_piaoquantv(oss_object_key, audio_title, pq_ids_list, cover, uid)
  424. if new_video_id:
  425. Common.logger("video").info(f"{mark}的{platform}渠道视频添加到对应用户成功")
  426. if os.path.isfile(v_oss_path):
  427. os.remove(v_oss_path)
  428. if os.path.isfile(v_path):
  429. os.remove(v_path)
  430. if os.path.isfile(s_path):
  431. os.remove(s_path)
  432. # 清空所有mp4数据
  433. cls.clear_mp4_files(video_path_url)
  434. count += 1
  435. current_time = datetime.now()
  436. formatted_time = current_time.strftime("%Y-%m-%d %H:%M:%S")
  437. if platform == "常规":
  438. third_chars = [j[2] for j in video_files]
  439. data = ",".join(third_chars)
  440. user_id = user
  441. else:
  442. user_id = video_id
  443. data = ''
  444. values = [[mark, str(uid), str(user_id), data, title, new_video_id, formatted_time]]
  445. if mark_name == "穆新艺":
  446. sheet = '50b8a1'
  447. elif mark_name == "信欣":
  448. sheet = 'UyVK7y'
  449. elif mark_name == "范军":
  450. sheet = 'uP3zbf'
  451. elif mark_name == "鲁涛":
  452. sheet = 'iDTHt4'
  453. elif mark_name == "余海涛":
  454. sheet = 'R1jIeT'
  455. elif mark_name == "罗情":
  456. sheet = 'iuxfAt'
  457. Feishu.insert_columns("LAn9so7E0hxRYht2UMEcK5wpnMj", sheet, "ROWS", 1, 2)
  458. random_wait_time = random.uniform(0.5, 2.5)
  459. time.sleep(random_wait_time)
  460. Feishu.update_values("LAn9so7E0hxRYht2UMEcK5wpnMj", sheet, "A2:Z2", values)
  461. except Exception as e:
  462. Common.logger("bk_video").warning(f"{mark}的视频拼接失败:{e}\n")
  463. continue
  464. if "-" in mark:
  465. name = mark.split("-")[0]
  466. else:
  467. name = mark
  468. Feishu.bot('recommend', 'AGC完成通知', f'今日{platform}任务拼接任务完成,共{count}条', name, mark_name)