support local videos

This commit is contained in:
harry
2024-04-27 08:33:44 +08:00
parent c161ab3124
commit 5d06530a39
10 changed files with 201 additions and 66 deletions

View File

@@ -6,3 +6,6 @@ PUNCTUATIONS = [
TASK_STATE_FAILED = -1 TASK_STATE_FAILED = -1
TASK_STATE_COMPLETE = 1 TASK_STATE_COMPLETE = 1
TASK_STATE_PROCESSING = 4 TASK_STATE_PROCESSING = 4
FILE_TYPE_VIDEOS = ['mp4', 'mov', 'mkv', 'webm']
FILE_TYPE_IMAGES = ['jpg', 'jpeg', 'png', 'bmp']

View File

@@ -1,6 +1,7 @@
from enum import Enum from enum import Enum
from typing import Any, Optional from typing import Any, Optional, List
import pydantic
from pydantic import BaseModel from pydantic import BaseModel
import warnings import warnings
@@ -28,6 +29,11 @@ class VideoAspect(str, Enum):
return 1080, 1920 return 1080, 1920
class _Config:
arbitrary_types_allowed = True
@pydantic.dataclasses.dataclass(config=_Config)
class MaterialInfo: class MaterialInfo:
provider: str = "pexels" provider: str = "pexels"
url: str = "" url: str = ""
@@ -95,6 +101,9 @@ class VideoParams(BaseModel):
video_clip_duration: Optional[int] = 5 video_clip_duration: Optional[int] = 5
video_count: Optional[int] = 1 video_count: Optional[int] = 1
video_source: Optional[str] = "pexels"
video_materials: Optional[List[MaterialInfo]] = None # 用于生成视频的素材
video_language: Optional[str] = "" # auto detect video_language: Optional[str] = "" # auto detect
voice_name: Optional[str] = "" voice_name: Optional[str] = ""

View File

@@ -114,14 +114,28 @@ def start(task_id, params: VideoParams):
sm.state.update_task(task_id, state=const.TASK_STATE_PROCESSING, progress=40) sm.state.update_task(task_id, state=const.TASK_STATE_PROCESSING, progress=40)
logger.info("\n\n## downloading videos") downloaded_videos = []
downloaded_videos = material.download_videos(task_id=task_id, if params.video_source == "local":
search_terms=video_terms, logger.info("\n\n## preprocess local materials")
video_aspect=params.video_aspect, materials = video.preprocess_video(materials=params.video_materials, clip_duration=max_clip_duration)
video_contact_mode=params.video_concat_mode, print(materials)
audio_duration=audio_duration * params.video_count,
max_clip_duration=max_clip_duration, if not materials:
) sm.state.update_task(task_id, state=const.TASK_STATE_FAILED)
logger.error("no valid materials found, please check the materials and try again.")
return
for material_info in materials:
print(material_info)
downloaded_videos.append(material_info.url)
else:
logger.info("\n\n## downloading videos")
downloaded_videos = material.download_videos(task_id=task_id,
search_terms=video_terms,
video_aspect=params.video_aspect,
video_contact_mode=params.video_concat_mode,
audio_duration=audio_duration * params.video_count,
max_clip_duration=max_clip_duration,
)
if not downloaded_videos: if not downloaded_videos:
sm.state.update_task(task_id, state=const.TASK_STATE_FAILED) sm.state.update_task(task_id, state=const.TASK_STATE_FAILED)
logger.error( logger.error(

View File

@@ -1,12 +1,13 @@
import glob import glob
import random import random
from typing import List from typing import List
from PIL import ImageFont from PIL import ImageFont, Image
from loguru import logger from loguru import logger
from moviepy.editor import * from moviepy.editor import *
from moviepy.video.tools.subtitles import SubtitlesClip from moviepy.video.tools.subtitles import SubtitlesClip
from app.models.schema import VideoAspect, VideoParams, VideoConcatMode from app.models import const
from app.models.schema import VideoAspect, VideoParams, VideoConcatMode, MaterialInfo
from app.utils import utils from app.utils import utils
@@ -268,55 +269,101 @@ def generate_video(video_path: str,
logger.success(f"completed") logger.success(f"completed")
def preprocess_video(materials: List[MaterialInfo], clip_duration=4):
for material in materials:
if not material.url:
continue
ext = utils.parse_extension(material.url)
try:
clip = VideoFileClip(material.url)
except Exception as e:
clip = ImageClip(material.url)
width = clip.size[0]
height = clip.size[1]
if width < 480 or height < 480:
logger.warning(f"video is too small, width: {width}, height: {height}")
continue
if ext in const.FILE_TYPE_IMAGES:
logger.info(f"processing image: {material.url}")
# 创建一个图片剪辑并设置持续时间为3秒钟
clip = ImageClip(material.url).set_duration(clip_duration).set_position("center")
# 使用resize方法来添加缩放效果。这里使用了lambda函数来使得缩放效果随时间变化。
# 假设我们想要从原始大小逐渐放大到120%的大小。
# t代表当前时间clip.duration为视频总时长这里是3秒。
# 注意1 表示100%的大小所以1.2表示120%的大小
zoom_clip = clip.resize(lambda t: 1 + (clip_duration * 0.03) * (t / clip.duration))
# 如果需要,可以创建一个包含缩放剪辑的复合视频剪辑
# (这在您想要在视频中添加其他元素时非常有用)
final_clip = CompositeVideoClip([zoom_clip])
# 输出视频
video_file = f"{material.url}.mp4"
final_clip.write_videofile(video_file, fps=30, logger=None)
final_clip.close()
material.url = video_file
logger.success(f"completed: {video_file}")
return materials
if __name__ == "__main__": if __name__ == "__main__":
txt_en = "Here's your guide to travel hacks for budget-friendly adventures" m = MaterialInfo()
txt_zh = "测试长字段这是您的旅行技巧指南帮助您进行预算友好的冒险" m.url = "/Users/harry/Downloads/IMG_2915.JPG"
font = utils.resource_dir() + "/fonts/STHeitiMedium.ttc" m.provider = "local"
for txt in [txt_en, txt_zh]: materials = preprocess_video([m], clip_duration=4)
t, h = wrap_text(text=txt, max_width=1000, font=font, fontsize=60) print(materials)
print(t)
task_id = "aa563149-a7ea-49c2-b39f-8c32cc225baf" # txt_en = "Here's your guide to travel hacks for budget-friendly adventures"
task_dir = utils.task_dir(task_id) # txt_zh = "测试长字段这是您的旅行技巧指南帮助您进行预算友好的冒险"
video_file = f"{task_dir}/combined-1.mp4" # font = utils.resource_dir() + "/fonts/STHeitiMedium.ttc"
audio_file = f"{task_dir}/audio.mp3" # for txt in [txt_en, txt_zh]:
subtitle_file = f"{task_dir}/subtitle.srt" # t, h = wrap_text(text=txt, max_width=1000, font=font, fontsize=60)
output_file = f"{task_dir}/final.mp4" # print(t)
# video_paths = []
# for file in os.listdir(utils.storage_dir("test")):
# if file.endswith(".mp4"):
# video_paths.append(os.path.join(utils.storage_dir("test"), file))
# #
# combine_videos(combined_video_path=video_file, # task_id = "aa563149-a7ea-49c2-b39f-8c32cc225baf"
# audio_file=audio_file, # task_dir = utils.task_dir(task_id)
# video_paths=video_paths, # video_file = f"{task_dir}/combined-1.mp4"
# video_aspect=VideoAspect.portrait, # audio_file = f"{task_dir}/audio.mp3"
# video_concat_mode=VideoConcatMode.random, # subtitle_file = f"{task_dir}/subtitle.srt"
# max_clip_duration=5, # output_file = f"{task_dir}/final.mp4"
# threads=2) #
# # video_paths = []
cfg = VideoParams() # # for file in os.listdir(utils.storage_dir("test")):
cfg.video_aspect = VideoAspect.portrait # # if file.endswith(".mp4"):
cfg.font_name = "STHeitiMedium.ttc" # # video_paths.append(os.path.join(utils.storage_dir("test"), file))
cfg.font_size = 60 # #
cfg.stroke_color = "#000000" # # combine_videos(combined_video_path=video_file,
cfg.stroke_width = 1.5 # # audio_file=audio_file,
cfg.text_fore_color = "#FFFFFF" # # video_paths=video_paths,
cfg.text_background_color = "transparent" # # video_aspect=VideoAspect.portrait,
cfg.bgm_type = "random" # # video_concat_mode=VideoConcatMode.random,
cfg.bgm_file = "" # # max_clip_duration=5,
cfg.bgm_volume = 1.0 # # threads=2)
cfg.subtitle_enabled = True #
cfg.subtitle_position = "bottom" # cfg = VideoParams()
cfg.n_threads = 2 # cfg.video_aspect = VideoAspect.portrait
cfg.paragraph_number = 1 # cfg.font_name = "STHeitiMedium.ttc"
# cfg.font_size = 60
cfg.voice_volume = 1.0 # cfg.stroke_color = "#000000"
# cfg.stroke_width = 1.5
generate_video(video_path=video_file, # cfg.text_fore_color = "#FFFFFF"
audio_path=audio_file, # cfg.text_background_color = "transparent"
subtitle_path=subtitle_file, # cfg.bgm_type = "random"
output_file=output_file, # cfg.bgm_file = ""
params=cfg # cfg.bgm_volume = 1.0
) # cfg.subtitle_enabled = True
# cfg.subtitle_position = "bottom"
# cfg.n_threads = 2
# cfg.paragraph_number = 1
#
# cfg.voice_volume = 1.0
#
# generate_video(video_path=video_file,
# audio_path=audio_file,
# subtitle_path=subtitle_file,
# output_file=output_file,
# params=cfg
# )

View File

@@ -67,10 +67,13 @@ def root_dir():
return os.path.dirname(os.path.dirname(os.path.dirname(os.path.realpath(__file__)))) return os.path.dirname(os.path.dirname(os.path.dirname(os.path.realpath(__file__))))
def storage_dir(sub_dir: str = ""): def storage_dir(sub_dir: str = "", create: bool = False):
d = os.path.join(root_dir(), "storage") d = os.path.join(root_dir(), "storage")
if sub_dir: if sub_dir:
d = os.path.join(d, sub_dir) d = os.path.join(d, sub_dir)
if create and not os.path.exists(d):
os.makedirs(d)
return d return d
@@ -219,3 +222,7 @@ def load_locales(i18n_dir):
with open(os.path.join(root, file), "r", encoding="utf-8") as f: with open(os.path.join(root, file), "r", encoding="utf-8") as f:
_locales[lang] = json.loads(f.read()) _locales[lang] = json.loads(f.read())
return _locales return _locales
def parse_extension(filename):
return os.path.splitext(filename)[1].strip().lower().replace(".", "")

View File

@@ -30,10 +30,11 @@ st.set_page_config(page_title="MoneyPrinterTurbo",
"video.\n\nhttps://github.com/harry0703/MoneyPrinterTurbo" "video.\n\nhttps://github.com/harry0703/MoneyPrinterTurbo"
}) })
from app.models.schema import VideoParams, VideoAspect, VideoConcatMode from app.models.schema import VideoParams, VideoAspect, VideoConcatMode, MaterialInfo
from app.services import task as tm, llm, voice from app.services import task as tm, llm, voice
from app.utils import utils from app.utils import utils
from app.config import config from app.config import config
from app.models.const import FILE_TYPE_VIDEOS, FILE_TYPE_IMAGES
hide_streamlit_style = """ hide_streamlit_style = """
<style>#root > div:nth-child(1) > div > div > div > div > section > div {padding-top: 0rem;}</style> <style>#root > div:nth-child(1) > div > div > div > div > section > div {padding-top: 0rem;}</style>
@@ -150,6 +151,8 @@ def tr(key):
st.write(tr("Get Help")) st.write(tr("Get Help"))
llm_provider = config.app.get("llm_provider", "").lower()
if not config.app.get("hide_config", False): if not config.app.get("hide_config", False):
with st.expander(tr("Basic Settings"), expanded=False): with st.expander(tr("Basic Settings"), expanded=False):
config_panels = st.columns(3) config_panels = st.columns(3)
@@ -319,6 +322,7 @@ middle_panel = panel[1]
right_panel = panel[2] right_panel = panel[2]
params = VideoParams(video_subject="") params = VideoParams(video_subject="")
uploaded_files = []
with left_panel: with left_panel:
with st.container(border=True): with st.container(border=True):
@@ -372,6 +376,24 @@ with middle_panel:
(tr("Sequential"), "sequential"), (tr("Sequential"), "sequential"),
(tr("Random"), "random"), (tr("Random"), "random"),
] ]
video_sources = [
(tr("Pexels"), "pexels"),
(tr("Local file"), "local"),
(tr("TikTok"), "douyin"),
(tr("Bilibili"), "bilibili"),
(tr("Xiaohongshu"), "xiaohongshu"),
]
selected_index = st.selectbox(tr("Video Source"),
options=range(len(video_sources)), # 使用索引作为内部选项值
format_func=lambda x: video_sources[x][0] # 显示给用户的是标签
)
params.video_source = video_sources[selected_index][1]
if params.video_source == 'local':
_supported_types = FILE_TYPE_VIDEOS + FILE_TYPE_IMAGES
uploaded_files = st.file_uploader("Upload Local Files",
type=["mp4", "mov", "avi", "flv", "mkv", "jpg", "jpeg", "png"],
accept_multiple_files=True)
selected_index = st.selectbox(tr("Video Concat Mode"), selected_index = st.selectbox(tr("Video Concat Mode"),
index=1, index=1,
options=range(len(video_concat_modes)), # 使用索引作为内部选项值 options=range(len(video_concat_modes)), # 使用索引作为内部选项值
@@ -512,6 +534,19 @@ if start_button:
scroll_to_bottom() scroll_to_bottom()
st.stop() st.stop()
if uploaded_files:
local_videos_dir = utils.storage_dir("local_videos", create=True)
for file in uploaded_files:
file_path = os.path.join(local_videos_dir, f"{file.file_id}_{file.name}")
with open(file_path, "wb") as f:
f.write(file.getbuffer())
m = MaterialInfo()
m.provider = "local"
m.url = file_path
if not params.video_materials:
params.video_materials = []
params.video_materials.append(m)
log_container = st.empty() log_container = st.empty()
log_records = [] log_records = []

View File

@@ -61,6 +61,11 @@
"Model Name": "Model Name", "Model Name": "Model Name",
"Please Enter the LLM API Key": "Please Enter the **LLM API Key**", "Please Enter the LLM API Key": "Please Enter the **LLM API Key**",
"Please Enter the Pexels API Key": "Please Enter the **Pexels API Key**", "Please Enter the Pexels API Key": "Please Enter the **Pexels API Key**",
"Get Help": "If you need help, or have any questions, you can join discord for help: https://harryai.cc" "Get Help": "If you need help, or have any questions, you can join discord for help: https://harryai.cc",
"Video Source": "Video Source",
"TikTok": "TikTok (TikTok support is coming soon)",
"Bilibili": "Bilibili (Bilibili support is coming soon)",
"Xiaohongshu": "Xiaohongshu (Xiaohongshu support is coming soon)",
"Local file": "Local file"
} }
} }

View File

@@ -62,6 +62,11 @@
"Model Name": "Model Name", "Model Name": "Model Name",
"Please Enter the LLM API Key": "Please Enter the **LLM API Key**", "Please Enter the LLM API Key": "Please Enter the **LLM API Key**",
"Please Enter the Pexels API Key": "Please Enter the **Pexels API Key**", "Please Enter the Pexels API Key": "Please Enter the **Pexels API Key**",
"Get Help": "If you need help, or have any questions, you can join discord for help: https://harryai.cc" "Get Help": "If you need help, or have any questions, you can join discord for help: https://harryai.cc",
"Video Source": "Video Source",
"TikTok": "TikTok (TikTok support is coming soon)",
"Bilibili": "Bilibili (Bilibili support is coming soon)",
"Xiaohongshu": "Xiaohongshu (Xiaohongshu support is coming soon)",
"Local file": "Local file"
} }
} }

View File

@@ -62,6 +62,11 @@
"Model Name": "Tên Mô Hình", "Model Name": "Tên Mô Hình",
"Please Enter the LLM API Key": "Vui lòng Nhập **Khóa API LLM**", "Please Enter the LLM API Key": "Vui lòng Nhập **Khóa API LLM**",
"Please Enter the Pexels API Key": "Vui lòng Nhập **Khóa API Pexels**", "Please Enter the Pexels API Key": "Vui lòng Nhập **Khóa API Pexels**",
"Get Help": "Nếu bạn cần giúp đỡ hoặc có bất kỳ câu hỏi nào, bạn có thể tham gia discord để được giúp đỡ: https://harryai.cc" "Get Help": "Nếu bạn cần giúp đỡ hoặc có bất kỳ câu hỏi nào, bạn có thể tham gia discord để được giúp đỡ: https://harryai.cc",
"Video Source": "Video Source",
"TikTok": "TikTok (TikTok support is coming soon)",
"Bilibili": "Bilibili (Bilibili support is coming soon)",
"Xiaohongshu": "Xiaohongshu (Xiaohongshu support is coming soon)",
"Local file": "Local file"
} }
} }

View File

@@ -62,6 +62,11 @@
"Model Name": "模型名称 (:blue[需要到大模型提供商的后台确认被授权的模型名称])", "Model Name": "模型名称 (:blue[需要到大模型提供商的后台确认被授权的模型名称])",
"Please Enter the LLM API Key": "请先填写大模型 **API Key**", "Please Enter the LLM API Key": "请先填写大模型 **API Key**",
"Please Enter the Pexels API Key": "请先填写 **Pexels API Key**", "Please Enter the Pexels API Key": "请先填写 **Pexels API Key**",
"Get Help": "有任何问题或建议,可以加入 **微信群** 求助或讨论https://harryai.cc" "Get Help": "有任何问题或建议,可以加入 **微信群** 求助或讨论https://harryai.cc",
"Video Source": "视频来源",
"TikTok": "抖音 (TikTok 支持中,敬请期待)",
"Bilibili": "哔哩哔哩 (Bilibili 支持中,敬请期待)",
"Xiaohongshu": "小红书 (Xiaohongshu 支持中,敬请期待)",
"Local file": "本地文件"
} }
} }