diff --git a/README.md b/README.md
index 47a4623..38c7da3 100644
--- a/README.md
+++ b/README.md
@@ -57,7 +57,7 @@
- [x] 支持 **多种语音** 合成
- [x] 支持 **字幕生成**,可以调整 `字体`、`位置`、`颜色`、`大小`,同时支持`字幕描边`设置
- [x] 支持 **背景音乐**,随机或者指定音乐文件,可设置`背景音乐音量`
-- [x] 视频素材来源 **高清**,而且 **无版权**
+- [x] 视频素材来源 **高清**,而且 **无版权**,也可以使用自己的本地素材
- [x] 支持 **OpenAI**、**moonshot**、**Azure**、**gpt4free**、**one-api**、**通义千问**、**Google Gemini**、**Ollama** 等多种模型接入
❓[如何使用免费的 **OpenAI GPT-3.5
@@ -71,7 +71,6 @@
- [ ] 增加更多视频素材来源,优化视频素材和文案的匹配度
- [ ] 增加视频长度选项:短、中、长
- [ ] 增加免费网络代理,让访问OpenAI和素材下载不再受限
-- [ ] 可以使用自己的素材
- [ ] 朗读声音和背景音乐,提供实时试听
- [ ] 支持更多的语音合成服务商,比如 OpenAI TTS
- [ ] 自动上传到YouTube平台
diff --git a/app/config/config.py b/app/config/config.py
index 7c2de5f..1d53c1f 100644
--- a/app/config/config.py
+++ b/app/config/config.py
@@ -56,7 +56,7 @@ listen_port = _cfg.get("listen_port", 8080)
project_name = _cfg.get("project_name", "MoneyPrinterTurbo")
project_description = _cfg.get("project_description",
"https://github.com/harry0703/MoneyPrinterTurbo")
-project_version = _cfg.get("project_version", "1.1.4")
+project_version = _cfg.get("project_version", "1.1.5")
reload_debug = False
imagemagick_path = app.get("imagemagick_path", "")
diff --git a/app/models/const.py b/app/models/const.py
index 2a56585..2c62c95 100644
--- a/app/models/const.py
+++ b/app/models/const.py
@@ -6,3 +6,6 @@ PUNCTUATIONS = [
TASK_STATE_FAILED = -1
TASK_STATE_COMPLETE = 1
TASK_STATE_PROCESSING = 4
+
+FILE_TYPE_VIDEOS = ['mp4', 'mov', 'mkv', 'webm']
+FILE_TYPE_IMAGES = ['jpg', 'jpeg', 'png', 'bmp']
diff --git a/app/models/schema.py b/app/models/schema.py
index 50ee918..e7d6576 100644
--- a/app/models/schema.py
+++ b/app/models/schema.py
@@ -1,6 +1,7 @@
from enum import Enum
-from typing import Any, Optional
+from typing import Any, Optional, List
+import pydantic
from pydantic import BaseModel
import warnings
@@ -28,6 +29,11 @@ class VideoAspect(str, Enum):
return 1080, 1920
+class _Config:
+ arbitrary_types_allowed = True
+
+
+@pydantic.dataclasses.dataclass(config=_Config)
class MaterialInfo:
provider: str = "pexels"
url: str = ""
@@ -95,6 +101,9 @@ class VideoParams(BaseModel):
video_clip_duration: Optional[int] = 5
video_count: Optional[int] = 1
+ video_source: Optional[str] = "pexels"
+ video_materials: Optional[List[MaterialInfo]] = None # 用于生成视频的素材
+
video_language: Optional[str] = "" # auto detect
voice_name: Optional[str] = ""
diff --git a/app/services/task.py b/app/services/task.py
index bcbc89c..df8dd0a 100644
--- a/app/services/task.py
+++ b/app/services/task.py
@@ -114,14 +114,28 @@ def start(task_id, params: VideoParams):
sm.state.update_task(task_id, state=const.TASK_STATE_PROCESSING, progress=40)
- 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,
- )
+ downloaded_videos = []
+ if params.video_source == "local":
+ logger.info("\n\n## preprocess local materials")
+ materials = video.preprocess_video(materials=params.video_materials, clip_duration=max_clip_duration)
+ print(materials)
+
+ 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:
sm.state.update_task(task_id, state=const.TASK_STATE_FAILED)
logger.error(
diff --git a/app/services/video.py b/app/services/video.py
index 814be27..aab9046 100644
--- a/app/services/video.py
+++ b/app/services/video.py
@@ -1,12 +1,13 @@
import glob
import random
from typing import List
-from PIL import ImageFont
+from PIL import ImageFont, Image
from loguru import logger
from moviepy.editor import *
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
@@ -268,55 +269,101 @@ def generate_video(video_path: str,
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__":
- txt_en = "Here's your guide to travel hacks for budget-friendly adventures"
- txt_zh = "测试长字段这是您的旅行技巧指南帮助您进行预算友好的冒险"
- font = utils.resource_dir() + "/fonts/STHeitiMedium.ttc"
- for txt in [txt_en, txt_zh]:
- t, h = wrap_text(text=txt, max_width=1000, font=font, fontsize=60)
- print(t)
+ m = MaterialInfo()
+ m.url = "/Users/harry/Downloads/IMG_2915.JPG"
+ m.provider = "local"
+ materials = preprocess_video([m], clip_duration=4)
+ print(materials)
- task_id = "aa563149-a7ea-49c2-b39f-8c32cc225baf"
- task_dir = utils.task_dir(task_id)
- video_file = f"{task_dir}/combined-1.mp4"
- audio_file = f"{task_dir}/audio.mp3"
- subtitle_file = f"{task_dir}/subtitle.srt"
- output_file = f"{task_dir}/final.mp4"
-
- # 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))
+ # txt_en = "Here's your guide to travel hacks for budget-friendly adventures"
+ # txt_zh = "测试长字段这是您的旅行技巧指南帮助您进行预算友好的冒险"
+ # font = utils.resource_dir() + "/fonts/STHeitiMedium.ttc"
+ # for txt in [txt_en, txt_zh]:
+ # t, h = wrap_text(text=txt, max_width=1000, font=font, fontsize=60)
+ # print(t)
#
- # combine_videos(combined_video_path=video_file,
- # audio_file=audio_file,
- # video_paths=video_paths,
- # video_aspect=VideoAspect.portrait,
- # video_concat_mode=VideoConcatMode.random,
- # max_clip_duration=5,
- # threads=2)
-
- cfg = VideoParams()
- cfg.video_aspect = VideoAspect.portrait
- cfg.font_name = "STHeitiMedium.ttc"
- cfg.font_size = 60
- cfg.stroke_color = "#000000"
- cfg.stroke_width = 1.5
- cfg.text_fore_color = "#FFFFFF"
- cfg.text_background_color = "transparent"
- cfg.bgm_type = "random"
- cfg.bgm_file = ""
- 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
- )
+ # task_id = "aa563149-a7ea-49c2-b39f-8c32cc225baf"
+ # task_dir = utils.task_dir(task_id)
+ # video_file = f"{task_dir}/combined-1.mp4"
+ # audio_file = f"{task_dir}/audio.mp3"
+ # subtitle_file = f"{task_dir}/subtitle.srt"
+ # output_file = f"{task_dir}/final.mp4"
+ #
+ # # 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,
+ # # audio_file=audio_file,
+ # # video_paths=video_paths,
+ # # video_aspect=VideoAspect.portrait,
+ # # video_concat_mode=VideoConcatMode.random,
+ # # max_clip_duration=5,
+ # # threads=2)
+ #
+ # cfg = VideoParams()
+ # cfg.video_aspect = VideoAspect.portrait
+ # cfg.font_name = "STHeitiMedium.ttc"
+ # cfg.font_size = 60
+ # cfg.stroke_color = "#000000"
+ # cfg.stroke_width = 1.5
+ # cfg.text_fore_color = "#FFFFFF"
+ # cfg.text_background_color = "transparent"
+ # cfg.bgm_type = "random"
+ # cfg.bgm_file = ""
+ # 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
+ # )
diff --git a/app/utils/utils.py b/app/utils/utils.py
index b7041dc..cca2154 100644
--- a/app/utils/utils.py
+++ b/app/utils/utils.py
@@ -67,10 +67,13 @@ def root_dir():
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")
if sub_dir:
d = os.path.join(d, sub_dir)
+ if create and not os.path.exists(d):
+ os.makedirs(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:
_locales[lang] = json.loads(f.read())
return _locales
+
+
+def parse_extension(filename):
+ return os.path.splitext(filename)[1].strip().lower().replace(".", "")
diff --git a/webui/Main.py b/webui/Main.py
index 4a519de..307fd0c 100644
--- a/webui/Main.py
+++ b/webui/Main.py
@@ -30,10 +30,11 @@ st.set_page_config(page_title="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.utils import utils
from app.config import config
+from app.models.const import FILE_TYPE_VIDEOS, FILE_TYPE_IMAGES
hide_streamlit_style = """
@@ -150,6 +151,8 @@ def tr(key):
st.write(tr("Get Help"))
+llm_provider = config.app.get("llm_provider", "").lower()
+
if not config.app.get("hide_config", False):
with st.expander(tr("Basic Settings"), expanded=False):
config_panels = st.columns(3)
@@ -319,6 +322,7 @@ middle_panel = panel[1]
right_panel = panel[2]
params = VideoParams(video_subject="")
+uploaded_files = []
with left_panel:
with st.container(border=True):
@@ -372,6 +376,24 @@ with middle_panel:
(tr("Sequential"), "sequential"),
(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"),
index=1,
options=range(len(video_concat_modes)), # 使用索引作为内部选项值
@@ -512,6 +534,19 @@ if start_button:
scroll_to_bottom()
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_records = []
diff --git a/webui/i18n/de.json b/webui/i18n/de.json
index 433fe17..4a4cfd4 100644
--- a/webui/i18n/de.json
+++ b/webui/i18n/de.json
@@ -61,6 +61,11 @@
"Model Name": "Model Name",
"Please Enter the LLM API Key": "Please Enter the **LLM 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"
}
}
\ No newline at end of file
diff --git a/webui/i18n/en.json b/webui/i18n/en.json
index 6595b6b..5136b92 100644
--- a/webui/i18n/en.json
+++ b/webui/i18n/en.json
@@ -62,6 +62,11 @@
"Model Name": "Model Name",
"Please Enter the LLM API Key": "Please Enter the **LLM 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"
}
}
\ No newline at end of file
diff --git a/webui/i18n/vi.json b/webui/i18n/vi.json
index bb9bc35..e9be295 100644
--- a/webui/i18n/vi.json
+++ b/webui/i18n/vi.json
@@ -62,6 +62,11 @@
"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 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"
}
}
diff --git a/webui/i18n/zh.json b/webui/i18n/zh.json
index c81d936..3702f16 100644
--- a/webui/i18n/zh.json
+++ b/webui/i18n/zh.json
@@ -62,6 +62,11 @@
"Model Name": "模型名称 (:blue[需要到大模型提供商的后台确认被授权的模型名称])",
"Please Enter the LLM API Key": "请先填写大模型 **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": "本地文件"
}
}
\ No newline at end of file