mirror of
https://github.com/harry0703/MoneyPrinterTurbo.git
synced 2026-02-21 16:37:21 +08:00
commit
53ba189c1b
23
.dockerignore
Normal file
23
.dockerignore
Normal file
@ -0,0 +1,23 @@
|
||||
# Exclude common Python files and directories
|
||||
venv/
|
||||
__pycache__/
|
||||
*.pyc
|
||||
*.pyo
|
||||
*.pyd
|
||||
*.pyz
|
||||
*.pyw
|
||||
*.pyi
|
||||
*.egg-info/
|
||||
|
||||
# Exclude development and local files
|
||||
.env
|
||||
.env.*
|
||||
*.log
|
||||
*.db
|
||||
|
||||
# Exclude version control system files
|
||||
.git/
|
||||
.gitignore
|
||||
.svn/
|
||||
|
||||
storage/
|
||||
9
.gitignore
vendored
9
.gitignore
vendored
@ -1,3 +1,10 @@
|
||||
.DS_Store
|
||||
/config.toml
|
||||
/storage/
|
||||
/.idea/
|
||||
/.idea/
|
||||
/app/services/__pycache__
|
||||
/app/__pycache__/
|
||||
/app/config/__pycache__/
|
||||
/app/models/__pycache__/
|
||||
/app/utils/__pycache__/
|
||||
/*/__pycache__/*
|
||||
37
Dockerfile
Normal file
37
Dockerfile
Normal file
@ -0,0 +1,37 @@
|
||||
# Use an official Python runtime as a parent image
|
||||
FROM python:3.10-slim
|
||||
|
||||
# Set the working directory in the container
|
||||
WORKDIR /MoneyPrinterTurbo
|
||||
|
||||
ENV PYTHONPATH="/MoneyPrinterTurbo:$PYTHONPATH"
|
||||
|
||||
# Install system dependencies
|
||||
RUN apt-get update && apt-get install -y \
|
||||
git \
|
||||
imagemagick \
|
||||
ffmpeg \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Fix security policy for ImageMagick
|
||||
RUN sed -i '/<policy domain="path" rights="none" pattern="@\*"/d' /etc/ImageMagick-6/policy.xml
|
||||
|
||||
# Copy the current directory contents into the container at /MoneyPrinterTurbo
|
||||
COPY ./app ./app
|
||||
COPY ./webui ./webui
|
||||
COPY ./resource ./resource
|
||||
COPY ./requirements.txt ./requirements.txt
|
||||
COPY ./main.py ./main.py
|
||||
|
||||
# Install Python dependencies
|
||||
RUN pip install --no-cache-dir -r requirements.txt
|
||||
|
||||
# Expose the port the app runs on
|
||||
EXPOSE 8501
|
||||
|
||||
# Command to run the application
|
||||
CMD ["streamlit", "run", "./webui/Main.py","--browser.serverAddress=0.0.0.0","--server.enableCORS=True","--browser.gatherUsageStats=False"]
|
||||
|
||||
# At runtime, mount the config.toml file from the host into the container
|
||||
# using Docker volumes. Example usage:
|
||||
# docker run -v ./config.toml:/MoneyPrinterTurbo/config.toml -v ./storage:/MoneyPrinterTurbo/storage -p 8501:8501 moneyprinterturbo
|
||||
200
README-en.md
200
README-en.md
@ -1,21 +1,38 @@
|
||||
# MoneyPrinterTurbo 💸
|
||||
<div align="center">
|
||||
<h1 align="center">MoneyPrinterTurbo 💸</h1>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://github.com/harry0703/MoneyPrinterTurbo/stargazers"><img src="https://img.shields.io/github/stars/harry0703/MoneyPrinterTurbo.svg?style=for-the-badge" alt="Stargazers"></a>
|
||||
<a href="https://github.com/harry0703/MoneyPrinterTurbo/issues"><img src="https://img.shields.io/github/issues/harry0703/MoneyPrinterTurbo.svg?style=for-the-badge" alt="Issues"></a>
|
||||
<a href="https://github.com/harry0703/MoneyPrinterTurbo/network/members"><img src="https://img.shields.io/github/forks/harry0703/MoneyPrinterTurbo.svg?style=for-the-badge" alt="Forks"></a>
|
||||
<a href="https://github.com/harry0703/MoneyPrinterTurbo/blob/main/LICENSE"><img src="https://img.shields.io/github/license/harry0703/MoneyPrinterTurbo.svg?style=for-the-badge" alt="License"></a>
|
||||
</p>
|
||||
|
||||
<h3>English | <a href="README.md">简体中文</a></h3>
|
||||
|
||||
[Chinese 简体中文](README.md)
|
||||
|
||||
> Thanks to [RootFTW](https://github.com/Root-FTW) for the translation
|
||||
|
||||
|
||||
Simply provide a **topic** or **keyword** for a video, and it will automatically generate the video copy, video
|
||||
Simply provide a <b>topic</b> or <b>keyword</b> for a video, and it will automatically generate the video copy, video
|
||||
materials, video subtitles, and video background music before synthesizing a high-definition short video.
|
||||
|
||||

|
||||
### WebUI
|
||||
|
||||

|
||||
|
||||
### API Interface
|
||||
|
||||

|
||||
|
||||
</div>
|
||||
|
||||
## Special Thanks 🙏
|
||||
|
||||
Due to the **deployment** and **usage** of this project, there is a certain threshold for some beginner users. We would
|
||||
like to express our special thanks to
|
||||
|
||||
**LuKa (AI Intelligent Multimedia Service Platform)** for providing a free `AI Video Generator` service based on this
|
||||
**RecCloud (AI-Powered Multimedia Service Platform)** for providing a free `AI Video Generator` service based on this
|
||||
project. It allows for online use without deployment, which is very convenient.
|
||||
|
||||
https://reccloud.com
|
||||
@ -24,7 +41,8 @@ https://reccloud.com
|
||||
|
||||
## Features 🎯
|
||||
|
||||
- [x] Complete **MVC architecture**, **clearly structured** code, easy to maintain, supports both API and Web interface
|
||||
- [x] Complete **MVC architecture**, **clearly structured** code, easy to maintain, supports both `API`
|
||||
and `Web interface`
|
||||
- [x] Supports **AI-generated** video copy, as well as **customized copy**
|
||||
- [x] Supports various **high-definition video** sizes
|
||||
- [x] Portrait 9:16, `1080x1920`
|
||||
@ -38,42 +56,108 @@ https://reccloud.com
|
||||
supports `subtitle outlining`
|
||||
- [x] Supports **background music**, either random or specified music files, with adjustable `background music volume`
|
||||
- [x] Video material sources are **high-definition** and **royalty-free**
|
||||
- [x] Supports integration with various models such as **OpenAI**, **moonshot**, **Azure**, **gpt4free**, **one-api**, *
|
||||
*qianwen**
|
||||
and more
|
||||
- [x] Supports integration with various models such as **OpenAI**, **moonshot**, **Azure**, **gpt4free**, **one-api**,
|
||||
**qianwen**, **Google Gemini** and more
|
||||
|
||||
### Future Plans 📅
|
||||
|
||||
- [ ] Support for GPT-SoVITS dubbing
|
||||
- [ ] Optimize voice synthesis using large models to make the synthesized voice sound more natural and emotionally rich
|
||||
- [ ] Add video transition effects to make the viewing experience smoother
|
||||
- [ ] Optimize the relevance of video materials
|
||||
- [ ] OLLAMA support
|
||||
- [ ] Introduce support for GPT-SoVITS dubbing
|
||||
- [ ] Enhance voice synthesis with large models for a more natural and emotionally resonant voice output
|
||||
- [ ] Incorporate video transition effects to ensure a smoother viewing experience
|
||||
- [ ] Improve the relevance of video content
|
||||
- [ ] Implement OLLAMA support
|
||||
- [ ] Add options for video length: short, medium, long
|
||||
- [ ] Package the application into a one-click launch bundle for Windows and macOS for ease of use
|
||||
- [ ] Enable the use of custom materials
|
||||
- [ ] Offer voiceover and background music options with real-time preview
|
||||
- [ ] Support a wider range of voice synthesis providers, such as OpenAI TTS
|
||||
- [ ] Automate the upload process to the YouTube platform
|
||||
|
||||
## Video Demos 📺
|
||||
|
||||
### Portrait 9:16
|
||||
|
||||
▶️ How to Add Fun to Your Life
|
||||
|
||||
https://github.com/harry0703/MoneyPrinterTurbo/assets/4928832/a84d33d5-27a2-4aba-8fd0-9fb2bd91c6a6
|
||||
|
||||
▶️ What is the Meaning of Life
|
||||
|
||||
https://github.com/harry0703/MoneyPrinterTurbo/assets/4928832/112c9564-d52b-4472-99ad-970b75f66476
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th align="center"><g-emoji class="g-emoji" alias="arrow_forward">▶️</g-emoji> How to Add Fun to Your Life </th>
|
||||
<th align="center"><g-emoji class="g-emoji" alias="arrow_forward">▶️</g-emoji> What is the Meaning of Life</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td align="center"><video src="https://github.com/harry0703/MoneyPrinterTurbo/assets/4928832/a84d33d5-27a2-4aba-8fd0-9fb2bd91c6a6"></video></td>
|
||||
<td align="center"><video src="https://github.com/harry0703/MoneyPrinterTurbo/assets/4928832/112c9564-d52b-4472-99ad-970b75f66476"></video></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
### Landscape 16:9
|
||||
|
||||
▶️ What is the Meaning of Life
|
||||
|
||||
https://github.com/harry0703/MoneyPrinterTurbo/assets/4928832/346ebb15-c55f-47a9-a653-114f08bb8073
|
||||
|
||||
▶️ Why Exercise
|
||||
|
||||
https://github.com/harry0703/MoneyPrinterTurbo/assets/4928832/271f2fae-8283-44a0-8aa0-0ed8f9a6fa87
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th align="center"><g-emoji class="g-emoji" alias="arrow_forward">▶️</g-emoji> What is the Meaning of Life</th>
|
||||
<th align="center"><g-emoji class="g-emoji" alias="arrow_forward">▶️</g-emoji> Why Exercise</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td align="center"><video src="https://github.com/harry0703/MoneyPrinterTurbo/assets/4928832/346ebb15-c55f-47a9-a653-114f08bb8073"></video></td>
|
||||
<td align="center"><video src="https://github.com/harry0703/MoneyPrinterTurbo/assets/4928832/271f2fae-8283-44a0-8aa0-0ed8f9a6fa87"></video></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
## Installation & Deployment 📥
|
||||
|
||||
- Try to avoid using **Chinese paths** to prevent unpredictable issues
|
||||
- Ensure your **network** is stable, meaning you can access foreign websites normally
|
||||
|
||||
#### ① Clone the Project
|
||||
|
||||
```shell
|
||||
git clone https://github.com/harry0703/MoneyPrinterTurbo.git
|
||||
```
|
||||
|
||||
#### ② Modify the Configuration File
|
||||
|
||||
- Copy the `config.example.toml` file and rename it to `config.toml`
|
||||
- Follow the instructions in the `config.toml` file to configure `pexels_api_keys` and `llm_provider`, and according to
|
||||
the llm_provider's service provider, set up the corresponding API Key
|
||||
|
||||
#### ③ Configure Large Language Models (LLM)
|
||||
|
||||
- To use `GPT-4.0` or `GPT-3.5`, you need an `API Key` from `OpenAI`. If you don't have one, you can set `llm_provider`
|
||||
to `g4f` (a free-to-use GPT library https://github.com/xtekky/gpt4free)
|
||||
|
||||
### Docker Deployment 🐳
|
||||
|
||||
#### ① Launch the Docker Container
|
||||
|
||||
If you haven't installed Docker, please install it first https://www.docker.com/products/docker-desktop/
|
||||
If you are using a Windows system, please refer to Microsoft's documentation:
|
||||
|
||||
1. https://learn.microsoft.com/en-us/windows/wsl/install
|
||||
2. https://learn.microsoft.com/en-us/windows/wsl/tutorials/wsl-containers
|
||||
|
||||
```shell
|
||||
cd MoneyPrinterTurbo
|
||||
docker-compose up
|
||||
```
|
||||
|
||||
#### ② Access the Web Interface
|
||||
|
||||
Open your browser and visit http://0.0.0.0:8501
|
||||
|
||||
#### ③ Access the API Interface
|
||||
|
||||
Open your browser and visit http://0.0.0.0:8080/docs Or http://0.0.0.0:8080/redoc
|
||||
|
||||
### Manual Deployment 📦
|
||||
|
||||
#### ① Create a Python Virtual Environment
|
||||
|
||||
It is recommended to create a Python virtual environment
|
||||
using [conda](https://conda.io/projects/conda/en/latest/user-guide/install/index.html)
|
||||
|
||||
@ -85,71 +169,45 @@ conda activate MoneyPrinterTurbo
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
|
||||
## Quick Start 🚀
|
||||
#### ② Install ImageMagick
|
||||
|
||||
### Video Tutorials
|
||||
|
||||
- Complete usage demonstration: https://v.douyin.com/iFhnwsKY/
|
||||
- How to deploy on Windows: https://v.douyin.com/iFyjoW3M
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- Try to avoid using **Chinese paths** to prevent unpredictable issues
|
||||
- Ensure your **network** is stable, meaning you can access foreign websites normally
|
||||
|
||||
#### ① Install ImageMagick
|
||||
|
||||
##### Windows:
|
||||
###### Windows:
|
||||
|
||||
- Download https://imagemagick.org/archive/binaries/ImageMagick-7.1.1-29-Q16-x64-static.exe
|
||||
- Install the downloaded ImageMagick, do not change the installation path
|
||||
- Install the downloaded ImageMagick, **do not change the installation path**
|
||||
- Modify the `config.toml` configuration file, set `imagemagick_path` to your actual installation path (if you didn't
|
||||
change the path during installation, just uncomment it)
|
||||
|
||||
##### MacOS:
|
||||
###### MacOS:
|
||||
|
||||
```shell
|
||||
brew install imagemagick
|
||||
````
|
||||
|
||||
##### Ubuntu
|
||||
###### Ubuntu
|
||||
|
||||
```shell
|
||||
sudo apt-get install imagemagick
|
||||
```
|
||||
|
||||
##### CentOS
|
||||
###### CentOS
|
||||
|
||||
```shell
|
||||
sudo yum install ImageMagick
|
||||
```
|
||||
|
||||
#### ② Modify the Configuration File
|
||||
|
||||
- Copy the `config.example.toml` file and rename it to `config.toml`
|
||||
- Follow the instructions in the `config.toml` file to configure `pexels_api_keys` and `llm_provider`, and according to
|
||||
the llm_provider's service provider, set up the corresponding API Key
|
||||
- If it's a `Windows` system, `imagemagick_path` is your actual installation path (if you didn't change the path during
|
||||
installation, just uncomment it)
|
||||
|
||||
#### ③ Configure Large Language Models (LLM)
|
||||
|
||||
- To use `GPT-4.0` or `GPT-3.5`, you need an `API Key` from `OpenAI`. If you don't have one, you can set `llm_provider`
|
||||
to `g4f` (a free-to-use GPT library https://github.com/xtekky/gpt4free)
|
||||
- Alternatively, you can apply at [Moonshot](https://platform.moonshot.cn/console/api-keys). Register to get 15 yuan of
|
||||
trial money, which allows for about 1500 conversations. Then set `llm_provider="moonshot"` and `moonshot_api_key`.
|
||||
Thanks to [@jerryblues](https://github.com/harry0703/MoneyPrinterTurbo/issues/8) for the suggestion
|
||||
|
||||
### Launch the Web Interface 🌐
|
||||
#### ③ Launch the Web Interface 🌐
|
||||
|
||||
Note that you need to execute the following commands in the `root directory` of the MoneyPrinterTurbo project
|
||||
|
||||
#### Windows
|
||||
###### Windows
|
||||
|
||||
```bat
|
||||
conda activate MoneyPrinterTurbo
|
||||
webui.bat
|
||||
```
|
||||
|
||||
#### MacOS or Linux
|
||||
###### MacOS or Linux
|
||||
|
||||
```shell
|
||||
conda activate MoneyPrinterTurbo
|
||||
@ -158,10 +216,7 @@ sh webui.sh
|
||||
|
||||
After launching, the browser will open automatically
|
||||
|
||||
The effect is shown in the following image:
|
||||

|
||||
|
||||
### Launch the API Service 🚀
|
||||
#### ④ Launch the API Service 🚀
|
||||
|
||||
```shell
|
||||
python main.py
|
||||
@ -170,9 +225,6 @@ python main.py
|
||||
After launching, you can view the `API documentation` at http://127.0.0.1:8080/docs and directly test the interface
|
||||
online for a quick experience.
|
||||
|
||||
The effect is shown in the following image:
|
||||

|
||||
|
||||
## Voice Synthesis 🗣
|
||||
|
||||
A list of all supported voices can be viewed here: [Voice List](./docs/voice-list.txt)
|
||||
@ -270,3 +322,7 @@ optimizations and added functionalities. Thanks to the original author for their
|
||||
## License 📝
|
||||
|
||||
Click to view the [`LICENSE`](LICENSE) file
|
||||
|
||||
## Star History
|
||||
|
||||
[](https://star-history.com/#harry0703/MoneyPrinterTurbo&Date)
|
||||
253
README.md
253
README.md
@ -1,11 +1,28 @@
|
||||
# MoneyPrinterTurbo 💸
|
||||
<div align="center">
|
||||
<h1 align="center">MoneyPrinterTurbo 💸</h1>
|
||||
|
||||
[English](README-en.md)
|
||||
<p align="center">
|
||||
<a href="https://github.com/harry0703/MoneyPrinterTurbo/stargazers"><img src="https://img.shields.io/github/stars/harry0703/MoneyPrinterTurbo.svg?style=for-the-badge" alt="Stargazers"></a>
|
||||
<a href="https://github.com/harry0703/MoneyPrinterTurbo/issues"><img src="https://img.shields.io/github/issues/harry0703/MoneyPrinterTurbo.svg?style=for-the-badge" alt="Issues"></a>
|
||||
<a href="https://github.com/harry0703/MoneyPrinterTurbo/network/members"><img src="https://img.shields.io/github/forks/harry0703/MoneyPrinterTurbo.svg?style=for-the-badge" alt="Forks"></a>
|
||||
<a href="https://github.com/harry0703/MoneyPrinterTurbo/blob/main/LICENSE"><img src="https://img.shields.io/github/license/harry0703/MoneyPrinterTurbo.svg?style=for-the-badge" alt="License"></a>
|
||||
</p>
|
||||
<br>
|
||||
<h3>简体中文 | <a href="README-en.md">English</a></h3>
|
||||
<br>
|
||||
只需提供一个视频 <b>主题</b> 或 <b>关键词</b> ,就可以全自动生成视频文案、视频素材、视频字幕、视频背景音乐,然后合成一个高清的短视频。
|
||||
<br>
|
||||
|
||||
只需提供一个视频 **主题** 或 **关键词** ,就可以全自动生成视频文案、视频素材、视频字幕、视频背景音乐,然后合成一个高清的短视频。
|
||||
<h4>Web界面</h4>
|
||||
|
||||

|
||||
|
||||
<h4>API界面</h4>
|
||||
|
||||

|
||||
|
||||
</div>
|
||||
|
||||
## 特别感谢 🙏
|
||||
|
||||
由于该项目的 **部署** 和 **使用**,对于一些小白用户来说,还是 **有一定的门槛**,在此特别感谢
|
||||
@ -19,7 +36,7 @@
|
||||
|
||||
## 功能特性 🎯
|
||||
|
||||
- [x] 完整的 **MVC架构**,代码 **结构清晰**,易于维护,支持API和Web界面
|
||||
- [x] 完整的 **MVC架构**,代码 **结构清晰**,易于维护,支持 `API` 和 `Web界面`
|
||||
- [x] 支持视频文案 **AI自动生成**,也可以**自定义文案**
|
||||
- [x] 支持多种 **高清视频** 尺寸
|
||||
- [x] 竖屏 9:16,`1080x1920`
|
||||
@ -31,40 +48,116 @@
|
||||
- [x] 支持 **字幕生成**,可以调整 `字体`、`位置`、`颜色`、`大小`,同时支持`字幕描边`设置
|
||||
- [x] 支持 **背景音乐**,随机或者指定音乐文件,可设置`背景音乐音量`
|
||||
- [x] 视频素材来源 **高清**,而且 **无版权**
|
||||
- [x] 支持 **OpenAI**、**moonshot**、**Azure**、**gpt4free**、**one-api**、**通义千问** 等多种模型接入
|
||||
- [x] 支持 **OpenAI**、**moonshot**、**Azure**、**gpt4free**、**one-api**、**通义千问**、**Google Gemini** 等多种模型接入
|
||||
|
||||
### 后期计划 📅
|
||||
|
||||
- [ ] GPT-SoVITS 配音支持
|
||||
- [ ] 优化语音合成,利用大模型,使其合成的声音,更加自然,情绪更加丰富
|
||||
- [ ] 增加视频转场效果,使其看起来更加的流畅
|
||||
- [ ] 优化视频素材的匹配度
|
||||
- [ ] 增加更多视频素材来源,优化视频素材和文案的匹配度
|
||||
- [ ] OLLAMA 支持
|
||||
- [ ] 增加视频长度选项:短、中、长
|
||||
- [ ] 打包成一键启动包(Windows,macOS),方便使用
|
||||
- [ ] 增加免费网络代理,让访问OpenAI和素材下载不再受限
|
||||
- [ ] 可以使用自己的素材
|
||||
- [ ] 朗读声音和背景音乐,提供实时试听
|
||||
- [ ] 支持更多的语音合成服务商,比如 OpenAI TTS
|
||||
- [ ] 自动上传到YouTube平台
|
||||
|
||||
## 视频演示 📺
|
||||
|
||||
### 竖屏 9:16
|
||||
|
||||
▶️ 《如何增加生活的乐趣》
|
||||
|
||||
https://github.com/harry0703/MoneyPrinterTurbo/assets/4928832/a84d33d5-27a2-4aba-8fd0-9fb2bd91c6a6
|
||||
|
||||
▶️ 《生命的意义是什么》
|
||||
|
||||
https://github.com/harry0703/MoneyPrinterTurbo/assets/4928832/112c9564-d52b-4472-99ad-970b75f66476
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th align="center"><g-emoji class="g-emoji" alias="arrow_forward">▶️</g-emoji> 《如何增加生活的乐趣》</th>
|
||||
<th align="center"><g-emoji class="g-emoji" alias="arrow_forward">▶️</g-emoji> 《生命的意义是什么》</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td align="center"><video src="https://github.com/harry0703/MoneyPrinterTurbo/assets/4928832/a84d33d5-27a2-4aba-8fd0-9fb2bd91c6a6"></video></td>
|
||||
<td align="center"><video src="https://github.com/harry0703/MoneyPrinterTurbo/assets/4928832/112c9564-d52b-4472-99ad-970b75f66476"></video></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
### 横屏 16:9
|
||||
|
||||
▶️《生命的意义是什么》
|
||||
|
||||
https://github.com/harry0703/MoneyPrinterTurbo/assets/4928832/346ebb15-c55f-47a9-a653-114f08bb8073
|
||||
|
||||
▶️《为什么要运动》
|
||||
|
||||
https://github.com/harry0703/MoneyPrinterTurbo/assets/4928832/271f2fae-8283-44a0-8aa0-0ed8f9a6fa87
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th align="center"><g-emoji class="g-emoji" alias="arrow_forward">▶️</g-emoji>《生命的意义是什么》</th>
|
||||
<th align="center"><g-emoji class="g-emoji" alias="arrow_forward">▶️</g-emoji>《为什么要运动》</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td align="center"><video src="https://github.com/harry0703/MoneyPrinterTurbo/assets/4928832/346ebb15-c55f-47a9-a653-114f08bb8073"></video></td>
|
||||
<td align="center"><video src="https://github.com/harry0703/MoneyPrinterTurbo/assets/4928832/271f2fae-8283-44a0-8aa0-0ed8f9a6fa87"></video></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
## 安装部署 📥
|
||||
|
||||
- 尽量不要使用 **中文路径**,避免出现一些无法预料的问题
|
||||
- 请确保你的 **网络** 是正常的,VPN需要打开`全局流量`模式
|
||||
|
||||
#### ① 克隆代码
|
||||
|
||||
```shell
|
||||
git clone https://github.com/harry0703/MoneyPrinterTurbo.git
|
||||
```
|
||||
|
||||
#### ② 修改配置文件
|
||||
|
||||
- 将 `config.example.toml` 文件复制一份,命名为 `config.toml`
|
||||
- 按照 `config.toml` 文件中的说明,配置好 `pexels_api_keys` 和 `llm_provider`,并根据 llm_provider 对应的服务商,配置相关的
|
||||
API Key
|
||||
|
||||
#### ③ 配置大模型(LLM)
|
||||
|
||||
- 如果要使用 `GPT-4.0` 或 `GPT-3.5`,需要有 `OpenAI` 的 `API Key`,如果没有,可以将 `llm_provider` 设置为 `g4f` (
|
||||
一个免费使用GPT的开源库 https://github.com/xtekky/gpt4free ,但是该免费的服务,稳定性较差,有时候可以用,有时候用不了)
|
||||
- 或者可以使用到 [月之暗面](https://platform.moonshot.cn/console/api-keys) 申请。注册就送
|
||||
15元体验金,可以对话1500次左右。然后设置 `llm_provider="moonshot"` 和 `moonshot_api_key`
|
||||
- 也可以使用 通义千问,具体请看配置文件里面的注释说明
|
||||
|
||||
### Docker部署 🐳
|
||||
|
||||
#### ① 启动Docker
|
||||
|
||||
如果未安装 Docker,请先安装 https://www.docker.com/products/docker-desktop/
|
||||
|
||||
如果是Windows系统,请参考微软的文档:
|
||||
1. https://learn.microsoft.com/zh-cn/windows/wsl/install
|
||||
2. https://learn.microsoft.com/zh-cn/windows/wsl/tutorials/wsl-containers
|
||||
|
||||
```shell
|
||||
cd MoneyPrinterTurbo
|
||||
docker-compose up
|
||||
```
|
||||
|
||||
#### ② 访问Web界面
|
||||
|
||||
打开浏览器,访问 http://0.0.0.0:8501
|
||||
|
||||
#### ③ 访问API文档
|
||||
|
||||
打开浏览器,访问 http://0.0.0.0:8080/docs 或者 http://0.0.0.0:8080/redoc
|
||||
|
||||
### 手动部署 📦
|
||||
|
||||
> 视频教程
|
||||
|
||||
- 完整的使用演示:https://v.douyin.com/iFhnwsKY/
|
||||
- 如何在Windows上部署:https://v.douyin.com/iFyjoW3M
|
||||
|
||||
#### ① 创建虚拟环境
|
||||
|
||||
建议使用 [conda](https://conda.io/projects/conda/en/latest/user-guide/install/index.html) 创建 python 虚拟环境
|
||||
|
||||
```shell
|
||||
@ -75,91 +168,58 @@ conda activate MoneyPrinterTurbo
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
|
||||
## 快速使用 🚀
|
||||
#### ② 安装好 ImageMagick
|
||||
|
||||
### 视频教程
|
||||
|
||||
- 完整的使用演示:https://v.douyin.com/iFhnwsKY/
|
||||
- 如何在Windows上部署:https://v.douyin.com/iFyjoW3M
|
||||
|
||||
### 前提
|
||||
|
||||
- 尽量不要使用 **中文路径**,避免出现一些无法预料的问题
|
||||
- 请确保你的 **网络** 是正常的,即可以正常访问境外网站
|
||||
|
||||
#### ① 安装好 ImageMagick
|
||||
|
||||
##### Windows:
|
||||
###### Windows:
|
||||
|
||||
- 下载 https://imagemagick.org/archive/binaries/ImageMagick-7.1.1-29-Q16-x64-static.exe
|
||||
- 安装下载好的 ImageMagick,注意不要修改安装路径
|
||||
- 修改 `配置文件 config.toml` 中的 `imagemagick_path` 为你的实际安装路径(如果安装的时候没有修改路径,直接取消注释即可)
|
||||
|
||||
##### MacOS:
|
||||
###### MacOS:
|
||||
|
||||
```shell
|
||||
brew install imagemagick
|
||||
````
|
||||
|
||||
##### Ubuntu
|
||||
###### Ubuntu
|
||||
|
||||
```shell
|
||||
sudo apt-get install imagemagick
|
||||
```
|
||||
|
||||
##### CentOS
|
||||
###### CentOS
|
||||
|
||||
```shell
|
||||
sudo yum install ImageMagick
|
||||
```
|
||||
|
||||
#### ② 修改配置文件
|
||||
|
||||
- 将 `config.example.toml` 文件复制一份,命名为 `config.toml`
|
||||
- 按照 `config.toml` 文件中的说明,配置好 `pexels_api_keys` 和 `llm_provider`,并根据 llm_provider 对应的服务商,配置相关的
|
||||
API Key
|
||||
- 如果是`Windows`系统,`imagemagick_path` 为你的实际安装路径(如果安装的时候没有修改路径,直接取消注释即可)
|
||||
|
||||
#### ③ 配置大模型(LLM)
|
||||
|
||||
- 如果要使用 `GPT-4.0` 或 `GPT-3.5`,需要有 `OpenAI` 的 `API Key`,如果没有,可以将 `llm_provider` 设置为 `g4f` (
|
||||
一个免费使用GPT的开源库 https://github.com/xtekky/gpt4free)
|
||||
- 或者可以使用到 [月之暗面](https://platform.moonshot.cn/console/api-keys) 申请。注册就送
|
||||
15元体验金,可以对话1500次左右。然后设置 `llm_provider="moonshot"` 和 `moonshot_api_key`
|
||||
。感谢 [@jerryblues](https://github.com/harry0703/MoneyPrinterTurbo/issues/8) 的建议
|
||||
|
||||
### 启动Web界面 🌐
|
||||
#### ③ 启动Web界面 🌐
|
||||
|
||||
注意需要到 MoneyPrinterTurbo 项目 `根目录` 下执行以下命令
|
||||
|
||||
#### Windows
|
||||
###### Windows
|
||||
|
||||
```bat
|
||||
conda activate MoneyPrinterTurbo
|
||||
webui.bat
|
||||
```
|
||||
|
||||
#### MacOS or Linux
|
||||
###### MacOS or Linux
|
||||
|
||||
```shell
|
||||
conda activate MoneyPrinterTurbo
|
||||
sh webui.sh
|
||||
```
|
||||
|
||||
启动后,会自动打开浏览器
|
||||
|
||||
效果如下图:
|
||||

|
||||
|
||||
### 启动API服务 🚀
|
||||
#### ④ 启动API服务 🚀
|
||||
|
||||
```shell
|
||||
python main.py
|
||||
```
|
||||
|
||||
启动后,可以查看 `API文档` http://127.0.0.1:8080/docs 直接在线调试接口,快速体验。
|
||||
|
||||
效果如下图:
|
||||

|
||||
启动后,可以查看 `API文档` http://127.0.0.1:8080/docs 或者 http://127.0.0.1:8080/redoc 直接在线调试接口,快速体验。
|
||||
|
||||
## 语音合成 🗣
|
||||
|
||||
@ -170,13 +230,15 @@ python main.py
|
||||
当前支持2种字幕生成方式:
|
||||
|
||||
- edge: 生成速度更快,性能更好,对电脑配置没有要求,但是质量可能不稳定
|
||||
- whisper: 生成速度较慢,性能较差,对电脑配置有一定要求,但是质量更可靠
|
||||
- whisper: 生成速度较慢,性能较差,对电脑配置有一定要求,但是质量更可靠。
|
||||
|
||||
可以修改 `config.toml` 配置文件中的 `subtitle_provider` 进行切换
|
||||
|
||||
建议使用 `edge` 模式,如果生成的字幕质量不好,再切换到 `whisper` 模式
|
||||
|
||||
> 如果留空,表示不生成字幕。
|
||||
> 注意:
|
||||
1. whisper 模式下需要到 HuggingFace 下载一个模型文件,大约 3GB 左右,请确保网络通畅
|
||||
2. 如果留空,表示不生成字幕。
|
||||
|
||||
## 背景音乐 🎵
|
||||
|
||||
@ -189,6 +251,12 @@ python main.py
|
||||
|
||||
## 常见问题 🤔
|
||||
|
||||
### ❓AttributeError: 'str' object has no attribute 'choices'`
|
||||
|
||||
这个问题是由于 OpenAI 或者其他 LLM,没有返回正确的回复导致的。
|
||||
|
||||
大概率是网络原因, 使用 **VPN**,或者设置 `openai_base_url` 为你的代理 ,应该就可以解决了。
|
||||
|
||||
### ❓RuntimeError: No ffmpeg exe could be found
|
||||
|
||||
通常情况下,ffmpeg 会被自动下载,并且会被自动检测到。
|
||||
@ -239,6 +307,55 @@ if you are in China, please use a VPN.
|
||||
|
||||
感谢 [@wangwenqiao666](https://github.com/wangwenqiao666)的研究探索
|
||||
|
||||
### ❓ImageMagick的安全策略阻止了与临时文件@/tmp/tmpur5hyyto.txt相关的操作
|
||||
|
||||
[issue 92](https://github.com/harry0703/MoneyPrinterTurbo/issues/92)
|
||||
|
||||
可以在ImageMagick的配置文件policy.xml中找到这些策略。
|
||||
这个文件通常位于 /etc/ImageMagick-`X`/ 或 ImageMagick 安装目录的类似位置。
|
||||
修改包含`pattern="@"`的条目,将`rights="none"`更改为`rights="read|write"`以允许对文件的读写操作。
|
||||
|
||||
感谢 [@chenhengzh](https://github.com/chenhengzh)的研究探索
|
||||
|
||||
### ❓OSError: [Errno 24] Too many open files
|
||||
|
||||
[issue 100](https://github.com/harry0703/MoneyPrinterTurbo/issues/100)
|
||||
|
||||
这个问题是由于系统打开文件数限制导致的,可以通过修改系统的文件打开数限制来解决。
|
||||
|
||||
查看当前限制
|
||||
|
||||
```shell
|
||||
ulimit -n
|
||||
```
|
||||
|
||||
如果过低,可以调高一些,比如
|
||||
|
||||
```shell
|
||||
ulimit -n 10240
|
||||
```
|
||||
|
||||
### ❓AttributeError: module 'PIL.Image' has no attribute 'ANTIALIAS'
|
||||
|
||||
[issue 101](https://github.com/harry0703/MoneyPrinterTurbo/issues/101),
|
||||
[issue 83](https://github.com/harry0703/MoneyPrinterTurbo/issues/83),
|
||||
[issue 70](https://github.com/harry0703/MoneyPrinterTurbo/issues/70)
|
||||
|
||||
先看下当前的 Pillow 版本是多少
|
||||
|
||||
```shell
|
||||
pip list |grep Pillow
|
||||
```
|
||||
|
||||
如果是 10.x 的版本,可以尝试下降级看看,有用户反馈降级后正常
|
||||
|
||||
```shell
|
||||
pip uninstall Pillow
|
||||
pip install Pillow==9.5.0
|
||||
# 或者降级到 8.4.0
|
||||
pip install Pillow==8.4.0
|
||||
```
|
||||
|
||||
## 反馈建议 📢
|
||||
|
||||
- 可以提交 [issue](https://github.com/harry0703/MoneyPrinterTurbo/issues)
|
||||
@ -261,3 +378,7 @@ if you are in China, please use a VPN.
|
||||
|
||||
点击查看 [`LICENSE`](LICENSE) 文件
|
||||
|
||||
|
||||
## Star History
|
||||
|
||||
[](https://star-history.com/#harry0703/MoneyPrinterTurbo&Date)
|
||||
@ -46,6 +46,10 @@ def get_application() -> FastAPI:
|
||||
|
||||
|
||||
app = get_application()
|
||||
|
||||
task_dir = utils.task_dir()
|
||||
app.mount("/tasks", StaticFiles(directory=task_dir, html=True, follow_symlink=True), name="")
|
||||
|
||||
public_dir = utils.public_dir()
|
||||
app.mount("/", StaticFiles(directory=public_dir, html=True), name="")
|
||||
|
||||
|
||||
@ -5,6 +5,14 @@ from loguru import logger
|
||||
|
||||
root_dir = os.path.dirname(os.path.dirname(os.path.dirname(os.path.realpath(__file__))))
|
||||
config_file = f"{root_dir}/config.toml"
|
||||
if not os.path.isfile(config_file):
|
||||
example_file = f"{root_dir}/config.example.toml"
|
||||
if os.path.isfile(example_file):
|
||||
import shutil
|
||||
|
||||
shutil.copyfile(example_file, config_file)
|
||||
logger.info(f"copy config.example.toml to config.toml")
|
||||
|
||||
logger.info(f"load config from file: {config_file}")
|
||||
|
||||
with open(config_file, mode="rb") as fp:
|
||||
@ -20,8 +28,9 @@ log_level = _cfg.get("log_level", "DEBUG")
|
||||
listen_host = _cfg.get("listen_host", "0.0.0.0")
|
||||
listen_port = _cfg.get("listen_port", 8080)
|
||||
project_name = _cfg.get("project_name", "MoneyPrinterTurbo")
|
||||
project_description = _cfg.get("project_description", "MoneyPrinterTurbo\n by 抖音-网旭哈瑞.AI")
|
||||
project_version = _cfg.get("project_version", "1.0.0")
|
||||
project_description = _cfg.get("project_description",
|
||||
"<a href='https://github.com/harry0703/MoneyPrinterTurbo'>https://github.com/harry0703/MoneyPrinterTurbo</a>")
|
||||
project_version = _cfg.get("project_version", "1.0.1")
|
||||
reload_debug = False
|
||||
|
||||
imagemagick_path = app.get("imagemagick_path", "")
|
||||
|
||||
@ -1,13 +1,13 @@
|
||||
from os import path
|
||||
|
||||
from fastapi import Request, Depends, Path
|
||||
from fastapi import Request, Depends, Path, BackgroundTasks
|
||||
from loguru import logger
|
||||
|
||||
from app.config import config
|
||||
from app.controllers import base
|
||||
from app.controllers.v1.base import new_router
|
||||
from app.models.exception import HttpException
|
||||
from app.models.schema import TaskVideoRequest, TaskQueryResponse, TaskResponse, TaskQueryRequest
|
||||
from app.services import task as tm
|
||||
from app.services import state as sm
|
||||
from app.utils import utils
|
||||
|
||||
# 认证依赖项
|
||||
@ -15,30 +15,43 @@ from app.utils import utils
|
||||
router = new_router()
|
||||
|
||||
|
||||
@router.post("/videos", response_model=TaskResponse, summary="使用主题来生成短视频")
|
||||
def create_video(request: Request, body: TaskVideoRequest):
|
||||
@router.post("/videos", response_model=TaskResponse, summary="Generate a short video")
|
||||
def create_video(background_tasks: BackgroundTasks, request: Request, body: TaskVideoRequest):
|
||||
task_id = utils.get_uuid()
|
||||
request_id = base.get_task_id(request)
|
||||
try:
|
||||
task = {
|
||||
"task_id": task_id,
|
||||
"request_id": request_id,
|
||||
"params": body.dict(),
|
||||
}
|
||||
body_dict = body.dict()
|
||||
task.update(body_dict)
|
||||
result = tm.start(task_id=task_id, params=body)
|
||||
task["result"] = result
|
||||
sm.update_task(task_id)
|
||||
background_tasks.add_task(tm.start, task_id=task_id, params=body)
|
||||
logger.success(f"video created: {utils.to_json(task)}")
|
||||
return utils.get_response(200, task)
|
||||
except ValueError as e:
|
||||
raise HttpException(task_id=task_id, status_code=400, message=f"{request_id}: {str(e)}")
|
||||
|
||||
|
||||
@router.get("/tasks/{task_id}", response_model=TaskQueryResponse, summary="查询任务状态")
|
||||
def get_task(request: Request, task_id: str = Path(..., description="任务ID"),
|
||||
query: TaskQueryRequest = Depends()):
|
||||
@router.get("/tasks/{task_id}", response_model=TaskQueryResponse, summary="Query task status")
|
||||
def get_task(request: Request, task_id: str = Path(..., description="Task ID"),
|
||||
query: TaskQueryRequest = Depends()):
|
||||
endpoint = config.app.get("endpoint", "")
|
||||
if not endpoint:
|
||||
endpoint = str(request.base_url)
|
||||
endpoint = endpoint.rstrip("/")
|
||||
|
||||
request_id = base.get_task_id(request)
|
||||
data = query.dict()
|
||||
data["task_id"] = task_id
|
||||
raise HttpException(task_id=task_id, status_code=404,
|
||||
message=f"{request_id}: task not found", data=data)
|
||||
task = sm.get_task(task_id)
|
||||
if task:
|
||||
if "videos" in task:
|
||||
videos = task["videos"]
|
||||
task_dir = utils.task_dir()
|
||||
urls = []
|
||||
for v in videos:
|
||||
uri_path = v.replace(task_dir, "tasks")
|
||||
urls.append(f"{endpoint}/{uri_path}")
|
||||
task["videos"] = urls
|
||||
return utils.get_response(200, task)
|
||||
|
||||
raise HttpException(task_id=task_id, status_code=404, message=f"{request_id}: task not found")
|
||||
|
||||
@ -1,4 +1,8 @@
|
||||
punctuations = [
|
||||
"?", ",", ".", "、", ";", ":",
|
||||
"?", ",", "。", "、", ";", ":",
|
||||
PUNCTUATIONS = [
|
||||
"?", ",", ".", "、", ";", ":", "!", "…",
|
||||
"?", ",", "。", "、", ";", ":", "!", "...",
|
||||
]
|
||||
|
||||
TASK_STATE_FAILED = -1
|
||||
TASK_STATE_COMPLETE = 1
|
||||
TASK_STATE_PROCESSING = 4
|
||||
|
||||
@ -34,43 +34,43 @@ class MaterialInfo:
|
||||
duration: int = 0
|
||||
|
||||
|
||||
VoiceNames = [
|
||||
# zh-CN
|
||||
"female-zh-CN-XiaoxiaoNeural",
|
||||
"female-zh-CN-XiaoyiNeural",
|
||||
"female-zh-CN-liaoning-XiaobeiNeural",
|
||||
"female-zh-CN-shaanxi-XiaoniNeural",
|
||||
|
||||
"male-zh-CN-YunjianNeural",
|
||||
"male-zh-CN-YunxiNeural",
|
||||
"male-zh-CN-YunxiaNeural",
|
||||
"male-zh-CN-YunyangNeural",
|
||||
|
||||
# "female-zh-HK-HiuGaaiNeural",
|
||||
# "female-zh-HK-HiuMaanNeural",
|
||||
# "male-zh-HK-WanLungNeural",
|
||||
#
|
||||
# "female-zh-TW-HsiaoChenNeural",
|
||||
# "female-zh-TW-HsiaoYuNeural",
|
||||
# "male-zh-TW-YunJheNeural",
|
||||
|
||||
# en-US
|
||||
|
||||
"female-en-US-AnaNeural",
|
||||
"female-en-US-AriaNeural",
|
||||
"female-en-US-AvaNeural",
|
||||
"female-en-US-EmmaNeural",
|
||||
"female-en-US-JennyNeural",
|
||||
"female-en-US-MichelleNeural",
|
||||
|
||||
"male-en-US-AndrewNeural",
|
||||
"male-en-US-BrianNeural",
|
||||
"male-en-US-ChristopherNeural",
|
||||
"male-en-US-EricNeural",
|
||||
"male-en-US-GuyNeural",
|
||||
"male-en-US-RogerNeural",
|
||||
"male-en-US-SteffanNeural",
|
||||
]
|
||||
# VoiceNames = [
|
||||
# # zh-CN
|
||||
# "female-zh-CN-XiaoxiaoNeural",
|
||||
# "female-zh-CN-XiaoyiNeural",
|
||||
# "female-zh-CN-liaoning-XiaobeiNeural",
|
||||
# "female-zh-CN-shaanxi-XiaoniNeural",
|
||||
#
|
||||
# "male-zh-CN-YunjianNeural",
|
||||
# "male-zh-CN-YunxiNeural",
|
||||
# "male-zh-CN-YunxiaNeural",
|
||||
# "male-zh-CN-YunyangNeural",
|
||||
#
|
||||
# # "female-zh-HK-HiuGaaiNeural",
|
||||
# # "female-zh-HK-HiuMaanNeural",
|
||||
# # "male-zh-HK-WanLungNeural",
|
||||
# #
|
||||
# # "female-zh-TW-HsiaoChenNeural",
|
||||
# # "female-zh-TW-HsiaoYuNeural",
|
||||
# # "male-zh-TW-YunJheNeural",
|
||||
#
|
||||
# # en-US
|
||||
#
|
||||
# "female-en-US-AnaNeural",
|
||||
# "female-en-US-AriaNeural",
|
||||
# "female-en-US-AvaNeural",
|
||||
# "female-en-US-EmmaNeural",
|
||||
# "female-en-US-JennyNeural",
|
||||
# "female-en-US-MichelleNeural",
|
||||
#
|
||||
# "male-en-US-AndrewNeural",
|
||||
# "male-en-US-BrianNeural",
|
||||
# "male-en-US-ChristopherNeural",
|
||||
# "male-en-US-EricNeural",
|
||||
# "male-en-US-GuyNeural",
|
||||
# "male-en-US-RogerNeural",
|
||||
# "male-en-US-SteffanNeural",
|
||||
# ]
|
||||
|
||||
|
||||
class VideoParams:
|
||||
@ -97,7 +97,7 @@ class VideoParams:
|
||||
|
||||
video_language: Optional[str] = "" # auto detect
|
||||
|
||||
voice_name: Optional[str] = VoiceNames[0]
|
||||
voice_name: Optional[str] = ""
|
||||
bgm_type: Optional[str] = "random"
|
||||
bgm_file: Optional[str] = ""
|
||||
bgm_volume: Optional[float] = 0.2
|
||||
@ -136,10 +136,33 @@ class TaskQueryRequest(BaseModel):
|
||||
class TaskResponse(BaseResponse):
|
||||
class TaskResponseData(BaseModel):
|
||||
task_id: str
|
||||
task_type: str = ""
|
||||
|
||||
data: TaskResponseData
|
||||
|
||||
class Config:
|
||||
json_schema_extra = {
|
||||
"example": {
|
||||
"status": 200,
|
||||
"message": "success",
|
||||
"data": {
|
||||
"task_id": "6c85c8cc-a77a-42b9-bc30-947815aa0558"
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
class TaskQueryResponse(BaseResponse):
|
||||
pass
|
||||
class Config:
|
||||
json_schema_extra = {
|
||||
"example": {
|
||||
"status": 200,
|
||||
"message": "success",
|
||||
"data": {
|
||||
"state": 1,
|
||||
"progress": 100,
|
||||
"videos": [
|
||||
"http://127.0.0.1:8080/tasks/6c85c8cc-a77a-42b9-bc30-947815aa0558/final-1.mp4"
|
||||
]
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
@ -5,9 +5,9 @@ from typing import List
|
||||
from loguru import logger
|
||||
from openai import OpenAI
|
||||
from openai import AzureOpenAI
|
||||
import google.generativeai as genai
|
||||
from app.config import config
|
||||
|
||||
|
||||
def _generate_response(prompt: str) -> str:
|
||||
content = ""
|
||||
llm_provider = config.app.get("llm_provider", "openai")
|
||||
@ -42,6 +42,10 @@ def _generate_response(prompt: str) -> str:
|
||||
model_name = config.app.get("azure_model_name")
|
||||
base_url = config.app.get("azure_base_url", "")
|
||||
api_version = config.app.get("azure_api_version", "2024-02-15-preview")
|
||||
elif llm_provider == "gemini":
|
||||
api_key = config.app.get("gemini_api_key")
|
||||
model_name = config.app.get("gemini_model_name")
|
||||
base_url = "***"
|
||||
elif llm_provider == "qwen":
|
||||
api_key = config.app.get("qwen_api_key")
|
||||
model_name = config.app.get("qwen_model_name")
|
||||
@ -66,6 +70,44 @@ def _generate_response(prompt: str) -> str:
|
||||
content = response["output"]["text"]
|
||||
return content.replace("\n", "")
|
||||
|
||||
if llm_provider == "gemini":
|
||||
genai.configure(api_key=api_key)
|
||||
|
||||
generation_config = {
|
||||
"temperature": 0.5,
|
||||
"top_p": 1,
|
||||
"top_k": 1,
|
||||
"max_output_tokens": 2048,
|
||||
}
|
||||
|
||||
safety_settings = [
|
||||
{
|
||||
"category": "HARM_CATEGORY_HARASSMENT",
|
||||
"threshold": "BLOCK_ONLY_HIGH"
|
||||
},
|
||||
{
|
||||
"category": "HARM_CATEGORY_HATE_SPEECH",
|
||||
"threshold": "BLOCK_ONLY_HIGH"
|
||||
},
|
||||
{
|
||||
"category": "HARM_CATEGORY_SEXUALLY_EXPLICIT",
|
||||
"threshold": "BLOCK_ONLY_HIGH"
|
||||
},
|
||||
{
|
||||
"category": "HARM_CATEGORY_DANGEROUS_CONTENT",
|
||||
"threshold": "BLOCK_ONLY_HIGH"
|
||||
},
|
||||
]
|
||||
|
||||
model = genai.GenerativeModel(model_name=model_name,
|
||||
generation_config=generation_config,
|
||||
safety_settings=safety_settings)
|
||||
|
||||
convo = model.start_chat(history=[])
|
||||
|
||||
convo.send_message(prompt)
|
||||
return convo.last.text
|
||||
|
||||
if llm_provider == "azure":
|
||||
client = AzureOpenAI(
|
||||
api_key=api_key,
|
||||
@ -169,6 +211,8 @@ Generate {amount} search terms for stock videos, depending on the subject of a v
|
||||
|
||||
### Video Script
|
||||
{video_script}
|
||||
|
||||
Please note that you must use English for generating video search terms; Chinese is not accepted.
|
||||
""".strip()
|
||||
|
||||
logger.info(f"subject: {video_subject}")
|
||||
|
||||
@ -17,6 +17,10 @@ if not pexels_api_keys:
|
||||
|
||||
|
||||
def round_robin_api_key():
|
||||
# if only one key is provided, return it
|
||||
if isinstance(pexels_api_keys, str):
|
||||
return pexels_api_keys
|
||||
|
||||
global requested_count
|
||||
requested_count += 1
|
||||
return pexels_api_keys[requested_count % len(pexels_api_keys)]
|
||||
|
||||
35
app/services/state.py
Normal file
35
app/services/state.py
Normal file
@ -0,0 +1,35 @@
|
||||
# State Management
|
||||
# This module is responsible for managing the state of the application.
|
||||
import math
|
||||
|
||||
# 如果你部署在分布式环境中,你可能需要一个中心化的状态管理服务,比如 Redis 或者数据库。
|
||||
# 如果你的应用程序是单机的,你可以使用内存来存储状态。
|
||||
|
||||
# If you are deploying in a distributed environment, you might need a centralized state management service like Redis or a database.
|
||||
# If your application is single-node, you can use memory to store the state.
|
||||
|
||||
from app.models import const
|
||||
from app.utils import utils
|
||||
|
||||
_tasks = {}
|
||||
|
||||
|
||||
def update_task(task_id: str, state: int = const.TASK_STATE_PROCESSING, progress: int = 0, **kwargs):
|
||||
"""
|
||||
Set the state of the task.
|
||||
"""
|
||||
progress = int(progress)
|
||||
if progress > 100:
|
||||
progress = 100
|
||||
|
||||
_tasks[task_id] = {
|
||||
"state": state,
|
||||
"progress": progress,
|
||||
**kwargs,
|
||||
}
|
||||
|
||||
def get_task(task_id: str):
|
||||
"""
|
||||
Get the state of the task.
|
||||
"""
|
||||
return _tasks.get(task_id, None)
|
||||
@ -20,8 +20,7 @@ def create(audio_file, subtitle_file: str = ""):
|
||||
logger.info(f"loading model: {model_size}, device: {device}, compute_type: {compute_type}")
|
||||
model = WhisperModel(model_size_or_path=model_size,
|
||||
device=device,
|
||||
compute_type=compute_type,
|
||||
local_files_only=True)
|
||||
compute_type=compute_type)
|
||||
|
||||
logger.info(f"start, output file: {subtitle_file}")
|
||||
if not subtitle_file:
|
||||
@ -160,6 +159,7 @@ if __name__ == "__main__":
|
||||
task_id = "c12fd1e6-4b0a-4d65-a075-c87abe35a072"
|
||||
task_dir = utils.task_dir(task_id)
|
||||
subtitle_file = f"{task_dir}/subtitle.srt"
|
||||
audio_file = f"{task_dir}/audio.mp3"
|
||||
|
||||
subtitles = file_to_subtitles(subtitle_file)
|
||||
print(subtitles)
|
||||
@ -171,3 +171,6 @@ if __name__ == "__main__":
|
||||
script = s.get("script")
|
||||
|
||||
correct(subtitle_file, script)
|
||||
|
||||
subtitle_file = f"{task_dir}/subtitle-test.srt"
|
||||
create(audio_file, subtitle_file)
|
||||
|
||||
@ -6,24 +6,13 @@ from os import path
|
||||
from loguru import logger
|
||||
|
||||
from app.config import config
|
||||
from app.models.schema import VideoParams, VoiceNames, VideoConcatMode
|
||||
from app.models import const
|
||||
from app.models.schema import VideoParams, VideoConcatMode
|
||||
from app.services import llm, material, voice, video, subtitle
|
||||
from app.services import state as sm
|
||||
from app.utils import utils
|
||||
|
||||
|
||||
def _parse_voice(name: str):
|
||||
# "female-zh-CN-XiaoxiaoNeural",
|
||||
# remove first part split by "-"
|
||||
if name not in VoiceNames:
|
||||
name = VoiceNames[0]
|
||||
|
||||
parts = name.split("-")
|
||||
_lang = f"{parts[1]}-{parts[2]}"
|
||||
_voice = f"{_lang}-{parts[3]}"
|
||||
|
||||
return _voice, _lang
|
||||
|
||||
|
||||
def start(task_id, params: VideoParams):
|
||||
"""
|
||||
{
|
||||
@ -39,8 +28,10 @@ def start(task_id, params: VideoParams):
|
||||
}
|
||||
"""
|
||||
logger.info(f"start task: {task_id}")
|
||||
sm.update_task(task_id, state=const.TASK_STATE_PROCESSING, progress=5)
|
||||
|
||||
video_subject = params.video_subject
|
||||
voice_name, language = _parse_voice(params.voice_name)
|
||||
voice_name = voice.parse_voice_name(params.voice_name)
|
||||
paragraph_number = params.paragraph_number
|
||||
n_threads = params.n_threads
|
||||
max_clip_duration = params.video_clip_duration
|
||||
@ -53,6 +44,8 @@ def start(task_id, params: VideoParams):
|
||||
else:
|
||||
logger.debug(f"video script: \n{video_script}")
|
||||
|
||||
sm.update_task(task_id, state=const.TASK_STATE_PROCESSING, progress=10)
|
||||
|
||||
logger.info("\n\n## generating video terms")
|
||||
video_terms = params.video_terms
|
||||
if not video_terms:
|
||||
@ -70,16 +63,20 @@ def start(task_id, params: VideoParams):
|
||||
script_file = path.join(utils.task_dir(task_id), f"script.json")
|
||||
script_data = {
|
||||
"script": video_script,
|
||||
"search_terms": video_terms
|
||||
"search_terms": video_terms,
|
||||
"params": params,
|
||||
}
|
||||
|
||||
with open(script_file, "w", encoding="utf-8") as f:
|
||||
f.write(utils.to_json(script_data))
|
||||
|
||||
sm.update_task(task_id, state=const.TASK_STATE_PROCESSING, progress=20)
|
||||
|
||||
logger.info("\n\n## generating audio")
|
||||
audio_file = path.join(utils.task_dir(task_id), f"audio.mp3")
|
||||
sub_maker = voice.tts(text=video_script, voice_name=voice_name, voice_file=audio_file)
|
||||
if sub_maker is None:
|
||||
sm.update_task(task_id, state=const.TASK_STATE_FAILED)
|
||||
logger.error(
|
||||
"failed to generate audio, maybe the network is not available. if you are in China, please use a VPN.")
|
||||
return
|
||||
@ -87,6 +84,8 @@ def start(task_id, params: VideoParams):
|
||||
audio_duration = voice.get_audio_duration(sub_maker)
|
||||
audio_duration = math.ceil(audio_duration)
|
||||
|
||||
sm.update_task(task_id, state=const.TASK_STATE_PROCESSING, progress=30)
|
||||
|
||||
subtitle_path = ""
|
||||
if params.subtitle_enabled:
|
||||
subtitle_path = path.join(utils.task_dir(task_id), f"subtitle.srt")
|
||||
@ -98,11 +97,6 @@ def start(task_id, params: VideoParams):
|
||||
if not os.path.exists(subtitle_path):
|
||||
subtitle_fallback = True
|
||||
logger.warning("subtitle file not found, fallback to whisper")
|
||||
else:
|
||||
subtitle_lines = subtitle.file_to_subtitles(subtitle_path)
|
||||
if not subtitle_lines:
|
||||
logger.warning(f"subtitle file is invalid, fallback to whisper : {subtitle_path}")
|
||||
subtitle_fallback = True
|
||||
|
||||
if subtitle_provider == "whisper" or subtitle_fallback:
|
||||
subtitle.create(audio_file=audio_file, subtitle_file=subtitle_path)
|
||||
@ -114,6 +108,8 @@ def start(task_id, params: VideoParams):
|
||||
logger.warning(f"subtitle file is invalid: {subtitle_path}")
|
||||
subtitle_path = ""
|
||||
|
||||
sm.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,
|
||||
@ -123,15 +119,19 @@ def start(task_id, params: VideoParams):
|
||||
max_clip_duration=max_clip_duration,
|
||||
)
|
||||
if not downloaded_videos:
|
||||
sm.update_task(task_id, state=const.TASK_STATE_FAILED)
|
||||
logger.error(
|
||||
"failed to download videos, maybe the network is not available. if you are in China, please use a VPN.")
|
||||
return
|
||||
|
||||
sm.update_task(task_id, state=const.TASK_STATE_PROCESSING, progress=50)
|
||||
|
||||
final_video_paths = []
|
||||
video_concat_mode = params.video_concat_mode
|
||||
if params.video_count > 1:
|
||||
video_concat_mode = VideoConcatMode.random
|
||||
|
||||
_progress = 50
|
||||
for i in range(params.video_count):
|
||||
index = i + 1
|
||||
combined_video_path = path.join(utils.task_dir(task_id), f"combined-{index}.mp4")
|
||||
@ -144,6 +144,9 @@ def start(task_id, params: VideoParams):
|
||||
max_clip_duration=max_clip_duration,
|
||||
threads=n_threads)
|
||||
|
||||
_progress += 50 / params.video_count / 2
|
||||
sm.update_task(task_id, progress=_progress)
|
||||
|
||||
final_video_path = path.join(utils.task_dir(task_id), f"final-{index}.mp4")
|
||||
|
||||
logger.info(f"\n\n## generating video: {index} => {final_video_path}")
|
||||
@ -154,10 +157,16 @@ def start(task_id, params: VideoParams):
|
||||
output_file=final_video_path,
|
||||
params=params,
|
||||
)
|
||||
|
||||
_progress += 50 / params.video_count / 2
|
||||
sm.update_task(task_id, progress=_progress)
|
||||
|
||||
final_video_paths.append(final_video_path)
|
||||
|
||||
logger.success(f"task {task_id} finished, generated {len(final_video_paths)} videos.")
|
||||
|
||||
return {
|
||||
kwargs = {
|
||||
"videos": final_video_paths,
|
||||
}
|
||||
sm.update_task(task_id, state=const.TASK_STATE_COMPLETE, progress=100, **kwargs)
|
||||
return kwargs
|
||||
|
||||
@ -64,24 +64,34 @@ def combine_videos(combined_video_path: str,
|
||||
clip = clip.set_fps(30)
|
||||
|
||||
# Not all videos are same size, so we need to resize them
|
||||
# logger.info(f"{video_path}: size is {clip.w} x {clip.h}, expected {video_width} x {video_height}")
|
||||
if clip.w != video_width or clip.h != video_height:
|
||||
if round((clip.w / clip.h), 4) < 0.5625:
|
||||
clip = crop(clip,
|
||||
width=clip.w,
|
||||
height=round(clip.w / 0.5625),
|
||||
x_center=clip.w / 2,
|
||||
y_center=clip.h / 2
|
||||
)
|
||||
clip_w, clip_h = clip.size
|
||||
if clip_w != video_width or clip_h != video_height:
|
||||
clip_ratio = clip.w / clip.h
|
||||
video_ratio = video_width / video_height
|
||||
|
||||
if clip_ratio == video_ratio:
|
||||
# 等比例缩放
|
||||
clip = clip.resize((video_width, video_height))
|
||||
else:
|
||||
clip = crop(clip,
|
||||
width=round(0.5625 * clip.h),
|
||||
height=clip.h,
|
||||
x_center=clip.w / 2,
|
||||
y_center=clip.h / 2
|
||||
)
|
||||
logger.info(f"resizing video to {video_width} x {video_height}")
|
||||
clip = clip.resize((video_width, video_height))
|
||||
# 等比缩放视频
|
||||
if clip_ratio > video_ratio:
|
||||
# 按照目标宽度等比缩放
|
||||
scale_factor = video_width / clip_w
|
||||
else:
|
||||
# 按照目标高度等比缩放
|
||||
scale_factor = video_height / clip_h
|
||||
|
||||
new_width = int(clip_w * scale_factor)
|
||||
new_height = int(clip_h * scale_factor)
|
||||
clip_resized = clip.resize(newsize=(new_width, new_height))
|
||||
|
||||
background = ColorClip(size=(video_width, video_height), color=(0, 0, 0))
|
||||
clip = CompositeVideoClip([
|
||||
background.set_duration(clip.duration),
|
||||
clip_resized.set_position("center")
|
||||
])
|
||||
|
||||
logger.info(f"resizing video to {video_width} x {video_height}, clip size: {clip_w} x {clip_h}")
|
||||
|
||||
if clip.duration > max_clip_duration:
|
||||
clip = clip.subclip(0, max_clip_duration)
|
||||
@ -92,7 +102,8 @@ def combine_videos(combined_video_path: str,
|
||||
final_clip = concatenate_videoclips(clips)
|
||||
final_clip = final_clip.set_fps(30)
|
||||
logger.info(f"writing")
|
||||
final_clip.write_videofile(combined_video_path, threads=threads)
|
||||
# https://github.com/harry0703/MoneyPrinterTurbo/issues/111#issuecomment-2032354030
|
||||
final_clip.write_videofile(combined_video_path, threads=threads, logger=None)
|
||||
logger.success(f"completed")
|
||||
return combined_video_path
|
||||
|
||||
@ -222,7 +233,7 @@ def generate_video(video_path: str,
|
||||
|
||||
temp_output_file = f"{output_file}.temp.mp4"
|
||||
logger.info(f"writing to temp file: {temp_output_file}")
|
||||
result.write_videofile(temp_output_file, threads=params.n_threads or 2)
|
||||
result.write_videofile(temp_output_file, threads=params.n_threads or 2, logger=None)
|
||||
|
||||
video_clip = VideoFileClip(temp_output_file)
|
||||
|
||||
@ -242,7 +253,7 @@ def generate_video(video_path: str,
|
||||
video_clip = video_clip.set_duration(original_duration)
|
||||
|
||||
logger.info(f"encoding audio codec to aac")
|
||||
video_clip.write_videofile(output_file, audio_codec="aac", threads=params.n_threads or 2)
|
||||
video_clip.write_videofile(output_file, audio_codec="aac", threads=params.n_threads or 2, logger=None)
|
||||
|
||||
os.remove(temp_output_file)
|
||||
logger.success(f"completed")
|
||||
@ -262,6 +273,20 @@ if __name__ == "__main__":
|
||||
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(task_dir, 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"
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -149,7 +149,7 @@ def text_to_srt(idx: int, msg: str, start_time: float, end_time: float) -> str:
|
||||
|
||||
|
||||
def str_contains_punctuation(word):
|
||||
for p in const.punctuations:
|
||||
for p in const.PUNCTUATIONS:
|
||||
if p in word:
|
||||
return True
|
||||
return False
|
||||
@ -159,7 +159,7 @@ def split_string_by_punctuations(s):
|
||||
result = []
|
||||
txt = ""
|
||||
for char in s:
|
||||
if char not in const.punctuations:
|
||||
if char not in const.PUNCTUATIONS:
|
||||
txt += char
|
||||
else:
|
||||
result.append(txt.strip())
|
||||
|
||||
@ -16,12 +16,15 @@
|
||||
# g4f
|
||||
# azure
|
||||
# qwen (通义千问)
|
||||
# gemini
|
||||
llm_provider="openai"
|
||||
|
||||
########## OpenAI API Key
|
||||
# Visit https://openai.com/api/ for details on obtaining an API key.
|
||||
# Get your API key at https://platform.openai.com/api-keys
|
||||
openai_api_key = ""
|
||||
openai_base_url = "" # no need to set it unless you want to use your own proxy
|
||||
# No need to set it unless you want to use your own proxy
|
||||
openai_base_url = ""
|
||||
# Check your available models at https://platform.openai.com/account/limits
|
||||
openai_model_name = "gpt-4-turbo-preview"
|
||||
|
||||
########## Moonshot API Key
|
||||
@ -49,6 +52,10 @@
|
||||
azure_model_name="gpt-35-turbo" # replace with your model deployment name
|
||||
azure_api_version = "2024-02-15-preview"
|
||||
|
||||
########## Gemini API Key
|
||||
gemini_api_key=""
|
||||
gemini_model_name = "gemini-1.0-pro"
|
||||
|
||||
########## Qwen API Key
|
||||
# Visit https://dashscope.console.aliyun.com/apiKey to get your API key
|
||||
# Visit below links to get more details
|
||||
@ -90,6 +97,20 @@
|
||||
# ffmpeg_path = "C:\\Users\\harry\\Downloads\\ffmpeg.exe"
|
||||
#########################################################################################
|
||||
|
||||
# 当视频生成成功后,API服务提供的视频下载接入点,默认为当前服务的地址和监听端口
|
||||
# 比如 http://127.0.0.1:8080/tasks/6357f542-a4e1-46a1-b4c9-bf3bd0df5285/final-1.mp4
|
||||
# 如果你需要使用域名对外提供服务(一般会用nginx做代理),则可以设置为你的域名
|
||||
# 比如 https://xxxx.com/tasks/6357f542-a4e1-46a1-b4c9-bf3bd0df5285/final-1.mp4
|
||||
# endpoint="https://xxxx.com"
|
||||
|
||||
# When the video is successfully generated, the API service provides a download endpoint for the video, defaulting to the service's current address and listening port.
|
||||
# For example, http://127.0.0.1:8080/tasks/6357f542-a4e1-46a1-b4c9-bf3bd0df5285/final-1.mp4
|
||||
# If you need to provide the service externally using a domain name (usually done with nginx as a proxy), you can set it to your domain name.
|
||||
# For example, https://xxxx.com/tasks/6357f542-a4e1-46a1-b4c9-bf3bd0df5285/final-1.mp4
|
||||
# endpoint="https://xxxx.com"
|
||||
endpoint=""
|
||||
|
||||
|
||||
[whisper]
|
||||
# Only effective when subtitle_provider is "whisper"
|
||||
|
||||
|
||||
27
docker-compose.yml
Normal file
27
docker-compose.yml
Normal file
@ -0,0 +1,27 @@
|
||||
version: "3"
|
||||
|
||||
x-common-volumes: &common-volumes
|
||||
- ./config.toml:/MoneyPrinterTurbo/config.toml
|
||||
- ./storage:/MoneyPrinterTurbo/storage
|
||||
|
||||
services:
|
||||
webui:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile
|
||||
container_name: "webui"
|
||||
ports:
|
||||
- "8501:8501"
|
||||
command: ["streamlit", "run", "./webui/Main.py","--browser.serverAddress=0.0.0.0","--server.enableCORS=True","--browser.gatherUsageStats=False"]
|
||||
volumes: *common-volumes
|
||||
restart: always
|
||||
api:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile
|
||||
container_name: "api"
|
||||
ports:
|
||||
- "8080:8080"
|
||||
command: [ "python3", "main.py" ]
|
||||
volumes: *common-volumes
|
||||
restart: always
|
||||
BIN
docs/api.jpg
BIN
docs/api.jpg
Binary file not shown.
|
Before Width: | Height: | Size: 257 KiB After Width: | Height: | Size: 252 KiB |
BIN
docs/webui-en.jpg
Normal file
BIN
docs/webui-en.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 384 KiB |
BIN
docs/webui.jpg
BIN
docs/webui.jpg
Binary file not shown.
|
Before Width: | Height: | Size: 339 KiB After Width: | Height: | Size: 340 KiB |
2
main.py
2
main.py
@ -3,6 +3,6 @@ from loguru import logger
|
||||
from app.config import config
|
||||
|
||||
if __name__ == '__main__':
|
||||
logger.info("start server, docs: http://127.0.0.1:" + str(config.listen_port) + "/docs")
|
||||
logger.info("start server, docs: http://0.0.0.0:" + str(config.listen_port) + "/docs")
|
||||
uvicorn.run(app="app.asgi:app", host=config.listen_host, port=config.listen_port, reload=config.reload_debug,
|
||||
log_level="warning")
|
||||
|
||||
@ -13,4 +13,5 @@ urllib3~=2.2.1
|
||||
pillow~=9.5.0
|
||||
pydantic~=2.6.3
|
||||
g4f~=0.2.5.4
|
||||
dashscope~=1.15.0
|
||||
dashscope~=1.15.0
|
||||
google.generativeai~=0.4.1
|
||||
19
resource/public/index.html
Normal file
19
resource/public/index.html
Normal file
@ -0,0 +1,19 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>MoneyPrinterTurbo</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1>MoneyPrinterTurbo</h1>
|
||||
<a href="https://github.com/harry0703/MoneyPrinterTurbo">https://github.com/harry0703/MoneyPrinterTurbo</a>
|
||||
<p>
|
||||
只需提供一个视频 主题 或 关键词 ,就可以全自动生成视频文案、视频素材、视频字幕、视频背景音乐,然后合成一个高清的短视频。
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Simply provide a topic or keyword for a video, and it will automatically generate the video copy, video materials,
|
||||
video subtitles, and video background music before synthesizing a high-definition short video.
|
||||
</p>
|
||||
</body>
|
||||
</html>
|
||||
294
webui/Main.py
294
webui/Main.py
@ -1,29 +1,68 @@
|
||||
import json
|
||||
import locale
|
||||
import streamlit as st
|
||||
|
||||
st.set_page_config(page_title="MoneyPrinterTurbo", page_icon="🤖", layout="wide",
|
||||
initial_sidebar_state="auto")
|
||||
import sys
|
||||
import os
|
||||
from uuid import uuid4
|
||||
|
||||
import platform
|
||||
import streamlit.components.v1 as components
|
||||
import toml
|
||||
from loguru import logger
|
||||
from app.models.schema import VideoParams, VideoAspect, VoiceNames, VideoConcatMode
|
||||
from app.services import task as tm, llm
|
||||
|
||||
st.set_page_config(page_title="MoneyPrinterTurbo",
|
||||
page_icon="🤖",
|
||||
layout="wide",
|
||||
initial_sidebar_state="auto",
|
||||
menu_items={
|
||||
'Report a bug': "https://github.com/harry0703/MoneyPrinterTurbo/issues",
|
||||
'About': "# MoneyPrinterTurbo\nSimply provide a topic or keyword for a video, and it will "
|
||||
"automatically generate the video copy, video materials, video subtitles, "
|
||||
"and video background music before synthesizing a high-definition short "
|
||||
"video.\n\nhttps://github.com/harry0703/MoneyPrinterTurbo"
|
||||
})
|
||||
|
||||
from app.models.schema import VideoParams, VideoAspect, VideoConcatMode
|
||||
from app.services import task as tm, llm, voice
|
||||
from app.utils import utils
|
||||
|
||||
hide_streamlit_style = """
|
||||
<style>#root > div:nth-child(1) > div > div > div > div > section > div {padding-top: 0rem;}</style>
|
||||
"""
|
||||
st.markdown(hide_streamlit_style, unsafe_allow_html=True)
|
||||
st.title("MoneyPrinterTurbo")
|
||||
st.write(
|
||||
"⚠️ 先在 **config.toml** 中设置 `pexels_api_keys` 和 `llm_provider` 参数,根据不同的 llm_provider,配置对应的 **API KEY**"
|
||||
)
|
||||
|
||||
root_dir = os.path.dirname(os.path.dirname(os.path.realpath(__file__)))
|
||||
font_dir = os.path.join(root_dir, "resource", "fonts")
|
||||
song_dir = os.path.join(root_dir, "resource", "songs")
|
||||
i18n_dir = os.path.join(root_dir, "webui", "i18n")
|
||||
config_file = os.path.join(root_dir, "webui", ".streamlit", "webui.toml")
|
||||
|
||||
|
||||
def load_config() -> dict:
|
||||
try:
|
||||
return toml.load(config_file)
|
||||
except Exception as e:
|
||||
return {}
|
||||
|
||||
|
||||
cfg = load_config()
|
||||
|
||||
|
||||
def save_config():
|
||||
with open(config_file, "w", encoding="utf-8") as f:
|
||||
f.write(toml.dumps(cfg))
|
||||
|
||||
|
||||
def get_system_locale():
|
||||
try:
|
||||
loc = locale.getdefaultlocale()
|
||||
# zh_CN, zh_TW return zh
|
||||
# en_US, en_GB return en
|
||||
language_code = loc[0].split("_")[0]
|
||||
return language_code
|
||||
except Exception as e:
|
||||
return "en"
|
||||
|
||||
# st.session_state
|
||||
|
||||
if 'video_subject' not in st.session_state:
|
||||
st.session_state['video_subject'] = ''
|
||||
@ -31,6 +70,8 @@ if 'video_script' not in st.session_state:
|
||||
st.session_state['video_script'] = ''
|
||||
if 'video_terms' not in st.session_state:
|
||||
st.session_state['video_terms'] = ''
|
||||
if 'ui_language' not in st.session_state:
|
||||
st.session_state['ui_language'] = cfg.get("ui_language", get_system_locale())
|
||||
|
||||
|
||||
def get_all_fonts():
|
||||
@ -51,6 +92,36 @@ def get_all_songs():
|
||||
return songs
|
||||
|
||||
|
||||
def open_task_folder(task_id):
|
||||
try:
|
||||
sys = platform.system()
|
||||
path = os.path.join(root_dir, "storage", "tasks", task_id)
|
||||
if os.path.exists(path):
|
||||
if sys == 'Windows':
|
||||
os.system(f"start {path}")
|
||||
if sys == 'Darwin':
|
||||
os.system(f"open {path}")
|
||||
except Exception as e:
|
||||
logger.error(e)
|
||||
|
||||
|
||||
def scroll_to_bottom():
|
||||
js = f"""
|
||||
<script>
|
||||
console.log("scroll_to_bottom");
|
||||
function scroll(dummy_var_to_force_repeat_execution){{
|
||||
var sections = parent.document.querySelectorAll('section.main');
|
||||
console.log(sections);
|
||||
for(let index = 0; index<sections.length; index++) {{
|
||||
sections[index].scrollTop = sections[index].scrollHeight;
|
||||
}}
|
||||
}}
|
||||
scroll(1);
|
||||
</script>
|
||||
"""
|
||||
st.components.v1.html(js, height=0, width=0)
|
||||
|
||||
|
||||
def init_log():
|
||||
logger.remove()
|
||||
_lvl = "DEBUG"
|
||||
@ -82,114 +153,154 @@ def init_log():
|
||||
|
||||
init_log()
|
||||
|
||||
|
||||
def load_locales():
|
||||
locales = {}
|
||||
for root, dirs, files in os.walk(i18n_dir):
|
||||
for file in files:
|
||||
if file.endswith(".json"):
|
||||
lang = file.split(".")[0]
|
||||
with open(os.path.join(root, file), "r", encoding="utf-8") as f:
|
||||
locales[lang] = json.loads(f.read())
|
||||
return locales
|
||||
|
||||
|
||||
locales = load_locales()
|
||||
|
||||
|
||||
def tr(key):
|
||||
loc = locales.get(st.session_state['ui_language'], {})
|
||||
return loc.get("Translation", {}).get(key, key)
|
||||
|
||||
|
||||
display_languages = []
|
||||
selected_index = 0
|
||||
for i, code in enumerate(locales.keys()):
|
||||
display_languages.append(f"{code} - {locales[code].get('Language')}")
|
||||
if code == st.session_state['ui_language']:
|
||||
selected_index = i
|
||||
|
||||
selected_language = st.selectbox("Language", options=display_languages, label_visibility='collapsed',
|
||||
index=selected_index)
|
||||
if selected_language:
|
||||
code = selected_language.split(" - ")[0].strip()
|
||||
st.session_state['ui_language'] = code
|
||||
cfg['ui_language'] = code
|
||||
save_config()
|
||||
|
||||
panel = st.columns(3)
|
||||
left_panel = panel[0]
|
||||
middle_panel = panel[1]
|
||||
right_panel = panel[2]
|
||||
|
||||
# define cfg as VideoParams class
|
||||
cfg = VideoParams()
|
||||
params = VideoParams()
|
||||
|
||||
with left_panel:
|
||||
with st.container(border=True):
|
||||
st.write("**文案设置**")
|
||||
cfg.video_subject = st.text_input("视频主题(给定一个关键词,:red[AI自动生成]视频文案)",
|
||||
value=st.session_state['video_subject']).strip()
|
||||
st.write(tr("Video Script Settings"))
|
||||
params.video_subject = st.text_input(tr("Video Subject"),
|
||||
value=st.session_state['video_subject']).strip()
|
||||
|
||||
video_languages = [
|
||||
("自动判断(Auto detect)", ""),
|
||||
(tr("Auto Detect"), ""),
|
||||
]
|
||||
for lang in ["zh-CN", "zh-TW", "en-US"]:
|
||||
video_languages.append((lang, lang))
|
||||
for code in ["zh-CN", "zh-TW", "de-DE", "en-US"]:
|
||||
video_languages.append((code, code))
|
||||
|
||||
selected_index = st.selectbox("生成视频脚本的语言(:blue[一般情况AI会自动根据你输入的主题语言输出])",
|
||||
selected_index = st.selectbox(tr("Script Language"),
|
||||
index=0,
|
||||
options=range(len(video_languages)), # 使用索引作为内部选项值
|
||||
format_func=lambda x: video_languages[x][0] # 显示给用户的是标签
|
||||
)
|
||||
cfg.video_language = video_languages[selected_index][1]
|
||||
params.video_language = video_languages[selected_index][1]
|
||||
|
||||
if cfg.video_language:
|
||||
st.write(f"设置AI输出文案语言为: **:red[{cfg.video_language}]**")
|
||||
|
||||
if st.button("点击使用AI根据**主题**生成 【视频文案】 和 【视频关键词】", key="auto_generate_script"):
|
||||
with st.spinner("AI正在生成视频文案和关键词..."):
|
||||
script = llm.generate_script(video_subject=cfg.video_subject, language=cfg.video_language)
|
||||
terms = llm.generate_terms(cfg.video_subject, script)
|
||||
st.toast('AI生成成功')
|
||||
if st.button(tr("Generate Video Script and Keywords"), key="auto_generate_script"):
|
||||
with st.spinner(tr("Generating Video Script and Keywords")):
|
||||
script = llm.generate_script(video_subject=params.video_subject, language=params.video_language)
|
||||
terms = llm.generate_terms(params.video_subject, script)
|
||||
st.session_state['video_script'] = script
|
||||
st.session_state['video_terms'] = ", ".join(terms)
|
||||
|
||||
cfg.video_script = st.text_area(
|
||||
"视频文案(:blue[①可不填,使用AI生成 ②合理使用标点断句,有助于生成字幕])",
|
||||
params.video_script = st.text_area(
|
||||
tr("Video Script"),
|
||||
value=st.session_state['video_script'],
|
||||
height=230
|
||||
height=180
|
||||
)
|
||||
if st.button("点击使用AI根据**文案**生成【视频关键词】", key="auto_generate_terms"):
|
||||
if not cfg.video_script:
|
||||
st.error("请先填写视频文案")
|
||||
if st.button(tr("Generate Video Keywords"), key="auto_generate_terms"):
|
||||
if not params.video_script:
|
||||
st.error(tr("Please Enter the Video Subject"))
|
||||
st.stop()
|
||||
|
||||
with st.spinner("AI正在生成视频关键词..."):
|
||||
terms = llm.generate_terms(cfg.video_subject, cfg.video_script)
|
||||
st.toast('AI生成成功')
|
||||
with st.spinner(tr("Generating Video Keywords")):
|
||||
terms = llm.generate_terms(params.video_subject, params.video_script)
|
||||
st.session_state['video_terms'] = ", ".join(terms)
|
||||
|
||||
cfg.video_terms = st.text_area(
|
||||
"视频关键词(:blue[①可不填,使用AI生成 ②用**英文逗号**分隔,只支持英文])",
|
||||
params.video_terms = st.text_area(
|
||||
tr("Video Keywords"),
|
||||
value=st.session_state['video_terms'],
|
||||
height=50)
|
||||
|
||||
with middle_panel:
|
||||
with st.container(border=True):
|
||||
st.write("**视频设置**")
|
||||
st.write(tr("Video Settings"))
|
||||
video_concat_modes = [
|
||||
("顺序拼接", "sequential"),
|
||||
("随机拼接(推荐)", "random"),
|
||||
(tr("Sequential"), "sequential"),
|
||||
(tr("Random"), "random"),
|
||||
]
|
||||
selected_index = st.selectbox("视频拼接模式",
|
||||
selected_index = st.selectbox(tr("Video Concat Mode"),
|
||||
index=1,
|
||||
options=range(len(video_concat_modes)), # 使用索引作为内部选项值
|
||||
format_func=lambda x: video_concat_modes[x][0] # 显示给用户的是标签
|
||||
)
|
||||
cfg.video_concat_mode = VideoConcatMode(video_concat_modes[selected_index][1])
|
||||
params.video_concat_mode = VideoConcatMode(video_concat_modes[selected_index][1])
|
||||
|
||||
video_aspect_ratios = [
|
||||
("竖屏 9:16(抖音视频)", VideoAspect.portrait.value),
|
||||
("横屏 16:9(西瓜视频)", VideoAspect.landscape.value),
|
||||
# ("方形 1:1", VideoAspect.square.value)
|
||||
(tr("Portrait"), VideoAspect.portrait.value),
|
||||
(tr("Landscape"), VideoAspect.landscape.value),
|
||||
]
|
||||
selected_index = st.selectbox("视频比例",
|
||||
selected_index = st.selectbox(tr("Video Ratio"),
|
||||
options=range(len(video_aspect_ratios)), # 使用索引作为内部选项值
|
||||
format_func=lambda x: video_aspect_ratios[x][0] # 显示给用户的是标签
|
||||
)
|
||||
cfg.video_aspect = VideoAspect(video_aspect_ratios[selected_index][1])
|
||||
params.video_aspect = VideoAspect(video_aspect_ratios[selected_index][1])
|
||||
|
||||
cfg.video_clip_duration = st.selectbox("视频片段最大时长(秒)", options=[2, 3, 4, 5, 6], index=1)
|
||||
cfg.video_count = st.selectbox("同时生成视频数量", options=[1, 2, 3, 4, 5], index=0)
|
||||
params.video_clip_duration = st.selectbox(tr("Clip Duration"), options=[2, 3, 4, 5, 6], index=1)
|
||||
params.video_count = st.selectbox(tr("Number of Videos Generated Simultaneously"), options=[1, 2, 3, 4, 5],
|
||||
index=0)
|
||||
with st.container(border=True):
|
||||
st.write("**音频设置**")
|
||||
# 创建一个映射字典,将原始值映射到友好名称
|
||||
st.write(tr("Audio Settings"))
|
||||
voices = voice.get_all_voices(filter_locals=["zh-CN", "zh-HK", "zh-TW", "de-DE", "en-US"])
|
||||
friendly_names = {
|
||||
voice: voice.
|
||||
replace("female", "女性").
|
||||
replace("male", "男性").
|
||||
replace("zh-CN", "中文").
|
||||
replace("zh-HK", "香港").
|
||||
replace("zh-TW", "台湾").
|
||||
replace("en-US", "英文").
|
||||
replace("Female", tr("Female")).
|
||||
replace("Male", tr("Male")).
|
||||
replace("Neural", "") for
|
||||
voice in VoiceNames}
|
||||
selected_friendly_name = st.selectbox("朗读声音", options=list(friendly_names.values()))
|
||||
voice in voices}
|
||||
saved_voice_name = cfg.get("voice_name", "")
|
||||
saved_voice_name_index = 0
|
||||
if saved_voice_name in friendly_names:
|
||||
saved_voice_name_index = list(friendly_names.keys()).index(saved_voice_name)
|
||||
else:
|
||||
for i, voice in enumerate(voices):
|
||||
if voice.lower().startswith(st.session_state['ui_language'].lower()):
|
||||
saved_voice_name_index = i
|
||||
break
|
||||
|
||||
selected_friendly_name = st.selectbox(tr("Speech Synthesis"),
|
||||
options=list(friendly_names.values()),
|
||||
index=saved_voice_name_index)
|
||||
|
||||
voice_name = list(friendly_names.keys())[list(friendly_names.values()).index(selected_friendly_name)]
|
||||
cfg.voice_name = voice_name
|
||||
params.voice_name = voice_name
|
||||
cfg['voice_name'] = voice_name
|
||||
save_config()
|
||||
|
||||
bgm_options = [
|
||||
("无背景音乐 No BGM", ""),
|
||||
("随机背景音乐 Random BGM", "random"),
|
||||
("自定义背景音乐 Custom BGM", "custom"),
|
||||
(tr("No Background Music"), ""),
|
||||
(tr("Random Background Music"), "random"),
|
||||
(tr("Custom Background Music"), "custom"),
|
||||
]
|
||||
selected_index = st.selectbox("背景音乐",
|
||||
selected_index = st.selectbox(tr("Background Music"),
|
||||
index=1,
|
||||
options=range(len(bgm_options)), # 使用索引作为内部选项值
|
||||
format_func=lambda x: bgm_options[x][0] # 显示给用户的是标签
|
||||
@ -199,55 +310,53 @@ with middle_panel:
|
||||
|
||||
# 根据选择显示或隐藏组件
|
||||
if bgm_type == "custom":
|
||||
custom_bgm_file = st.text_input("请输入自定义背景音乐的文件路径:")
|
||||
custom_bgm_file = st.text_input(tr("Custom Background Music File"))
|
||||
if custom_bgm_file and os.path.exists(custom_bgm_file):
|
||||
cfg.bgm_file = custom_bgm_file
|
||||
params.bgm_file = custom_bgm_file
|
||||
# st.write(f":red[已选择自定义背景音乐]:**{custom_bgm_file}**")
|
||||
cfg.bgm_volume = st.selectbox("背景音乐音量(0.2表示20%,背景声音不宜过高)",
|
||||
options=[0.0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0], index=2)
|
||||
params.bgm_volume = st.selectbox(tr("Background Music Volume"),
|
||||
options=[0.0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0], index=2)
|
||||
|
||||
with right_panel:
|
||||
with st.container(border=True):
|
||||
st.write("**字幕设置**")
|
||||
cfg.subtitle_enabled = st.checkbox("生成字幕(若取消勾选,下面的设置都将不生效)", value=True)
|
||||
st.write(tr("Subtitle Settings"))
|
||||
params.subtitle_enabled = st.checkbox(tr("Enable Subtitles"), value=True)
|
||||
font_names = get_all_fonts()
|
||||
cfg.font_name = st.selectbox("字体", font_names)
|
||||
params.font_name = st.selectbox(tr("Font"), font_names)
|
||||
|
||||
subtitle_positions = [
|
||||
("顶部(top)", "top"),
|
||||
("居中(center)", "center"),
|
||||
("底部(bottom,推荐)", "bottom"),
|
||||
(tr("Top"), "top"),
|
||||
(tr("Center"), "center"),
|
||||
(tr("Bottom"), "bottom"),
|
||||
]
|
||||
selected_index = st.selectbox("字幕位置",
|
||||
selected_index = st.selectbox(tr("Position"),
|
||||
index=2,
|
||||
options=range(len(subtitle_positions)), # 使用索引作为内部选项值
|
||||
format_func=lambda x: subtitle_positions[x][0] # 显示给用户的是标签
|
||||
)
|
||||
cfg.subtitle_position = subtitle_positions[selected_index][1]
|
||||
params.subtitle_position = subtitle_positions[selected_index][1]
|
||||
|
||||
font_cols = st.columns([0.3, 0.7])
|
||||
with font_cols[0]:
|
||||
cfg.text_fore_color = st.color_picker("字幕颜色", "#FFFFFF")
|
||||
params.text_fore_color = st.color_picker(tr("Font Color"), "#FFFFFF")
|
||||
with font_cols[1]:
|
||||
cfg.font_size = st.slider("字幕大小", 30, 100, 60)
|
||||
params.font_size = st.slider(tr("Font Size"), 30, 100, 60)
|
||||
|
||||
stroke_cols = st.columns([0.3, 0.7])
|
||||
with stroke_cols[0]:
|
||||
cfg.stroke_color = st.color_picker("描边颜色", "#000000")
|
||||
params.stroke_color = st.color_picker(tr("Stroke Color"), "#000000")
|
||||
with stroke_cols[1]:
|
||||
cfg.stroke_width = st.slider("描边粗细", 0.0, 10.0, 1.5)
|
||||
params.stroke_width = st.slider(tr("Stroke Width"), 0.0, 10.0, 1.5)
|
||||
|
||||
start_button = st.button("开始生成视频", use_container_width=True, type="primary")
|
||||
start_button = st.button(tr("Generate Video"), use_container_width=True, type="primary")
|
||||
if start_button:
|
||||
task_id = str(uuid4())
|
||||
if not cfg.video_subject and not cfg.video_script:
|
||||
st.error("视频主题 或 视频文案,不能同时为空")
|
||||
if not params.video_subject and not params.video_script:
|
||||
st.error(tr("Video Script and Subject Cannot Both Be Empty"))
|
||||
scroll_to_bottom()
|
||||
st.stop()
|
||||
|
||||
st.write(cfg)
|
||||
|
||||
log_container = st.empty()
|
||||
|
||||
log_records = []
|
||||
|
||||
|
||||
@ -259,6 +368,11 @@ if start_button:
|
||||
|
||||
logger.add(log_received)
|
||||
|
||||
logger.info("开始生成视频")
|
||||
st.toast(tr("Generating Video"))
|
||||
logger.info(tr("Start Generating Video"))
|
||||
logger.info(utils.to_json(params))
|
||||
scroll_to_bottom()
|
||||
|
||||
tm.start(task_id=task_id, params=cfg)
|
||||
tm.start(task_id=task_id, params=params)
|
||||
open_task_folder(task_id)
|
||||
logger.info(tr("Video Generation Completed"))
|
||||
|
||||
51
webui/i18n/de.json
Normal file
51
webui/i18n/de.json
Normal file
@ -0,0 +1,51 @@
|
||||
{
|
||||
"Language": "German",
|
||||
"Translation": {
|
||||
"Video Script Settings": "**Drehbuch / Topic des Videos**",
|
||||
"Video Subject": "Worum soll es in dem Video gehen? (Geben Sie ein Keyword an, :red[Dank KI wird automatisch ein Drehbuch generieren])",
|
||||
"Script Language": "Welche Sprache soll zum Generieren von Drehbüchern verwendet werden? :red[KI generiert anhand dieses Begriffs das Drehbuch]",
|
||||
"Generate Video Script and Keywords": "Klicken Sie hier, um mithilfe von KI ein [Video Drehbuch] und [Video Keywords] basierend auf dem **Keyword** zu generieren.",
|
||||
"Auto Detect": "Automatisch erkennen",
|
||||
"Video Script": "Drehbuch (Storybook) (:blue[① Optional, KI generiert ② Die richtige Zeichensetzung hilft bei der Erstellung von Untertiteln])",
|
||||
"Generate Video Keywords": "Klicken Sie, um KI zum Generieren zu verwenden [Video Keywords] basierend auf dem **Drehbuch**",
|
||||
"Please Enter the Video Subject": "Bitte geben Sie zuerst das Drehbuch an",
|
||||
"Generating Video Script and Keywords": "KI generiert ein Drehbuch und Schlüsselwörter...",
|
||||
"Generating Video Keywords": "AI is generating video keywords...",
|
||||
"Video Keywords": "Video Schlüsselwörter (:blue[① Optional, KI generiert ② Verwende **, (Kommas)** zur Trennung der Wörter, in englischer Sprache])",
|
||||
"Video Settings": "**Video Einstellungen**",
|
||||
"Video Concat Mode": "Videoverkettungsmodus",
|
||||
"Random": "Zufällige Verkettung (empfohlen)",
|
||||
"Sequential": "Sequentielle Verkettung",
|
||||
"Video Ratio": "Video-Seitenverhältnis",
|
||||
"Portrait": "Portrait 9:16",
|
||||
"Landscape": "Landschaft 16:9",
|
||||
"Clip Duration": "Maximale Dauer einzelner Videoclips in sekunden",
|
||||
"Number of Videos Generated Simultaneously": "Anzahl der parallel generierten Videos",
|
||||
"Audio Settings": "**Audio Einstellungen**",
|
||||
"Speech Synthesis": "Sprachausgabe",
|
||||
"Male": "Männlich",
|
||||
"Female": "Weiblich",
|
||||
"Background Music": "Hintergrundmusik",
|
||||
"No Background Music": "Ohne Hintergrundmusik",
|
||||
"Random Background Music": "Zufällig erzeugte Hintergrundmusik",
|
||||
"Custom Background Music": "Benutzerdefinierte Hintergrundmusik",
|
||||
"Custom Background Music File": "Bitte gib den Pfad zur Musikdatei an:",
|
||||
"Background Music Volume": "Lautstärke: (0.2 entspricht 20%, sollte nicht zu laut sein)",
|
||||
"Subtitle Settings": "**Untertitel-Einstellungen**",
|
||||
"Enable Subtitles": "Untertitel aktivieren (Wenn diese Option deaktiviert ist, werden die Einstellungen nicht genutzt)",
|
||||
"Font": "Schriftart des Untertitels",
|
||||
"Position": "Ausrichtung des Untertitels",
|
||||
"Top": "Oben",
|
||||
"Center": "Mittig",
|
||||
"Bottom": "Unten (empfohlen)",
|
||||
"Font Size": "Schriftgröße für Untertitel",
|
||||
"Font Color": "Schriftfarbe",
|
||||
"Stroke Color": "Kontur",
|
||||
"Stroke Width": "Breite der Untertitelkontur",
|
||||
"Generate Video": "Generiere Videos durch KI",
|
||||
"Video Script and Subject Cannot Both Be Empty": "Das Video-Thema und Drehbuch dürfen nicht beide leer sein",
|
||||
"Generating Video": "Video wird erstellt, bitte warten...",
|
||||
"Start Generating Video": "Beginne mit der Generierung",
|
||||
"Video Generation Completed": "Video erfolgreich generiert"
|
||||
}
|
||||
}
|
||||
51
webui/i18n/en.json
Normal file
51
webui/i18n/en.json
Normal file
@ -0,0 +1,51 @@
|
||||
{
|
||||
"Language": "English",
|
||||
"Translation": {
|
||||
"Video Script Settings": "**Video Script Settings**",
|
||||
"Video Subject": "Video Subject (Provide a keyword, :red[AI will automatically generate] video script)",
|
||||
"Script Language": "Language for Generating Video Script (AI will automatically output based on the language of your subject)",
|
||||
"Generate Video Script and Keywords": "Click to use AI to generate [Video Script] and [Video Keywords] based on **subject**",
|
||||
"Auto Detect": "Auto Detect",
|
||||
"Video Script": "Video Script (:blue[① Optional, AI generated ② Proper punctuation helps with subtitle generation])",
|
||||
"Generate Video Keywords": "Click to use AI to generate [Video Keywords] based on **script**",
|
||||
"Please Enter the Video Subject": "Please Enter the Video Script First",
|
||||
"Generating Video Script and Keywords": "AI is generating video script and keywords...",
|
||||
"Generating Video Keywords": "AI is generating video keywords...",
|
||||
"Video Keywords": "Video Keywords (:blue[① Optional, AI generated ② Use **English commas** for separation, English only])",
|
||||
"Video Settings": "**Video Settings**",
|
||||
"Video Concat Mode": "Video Concatenation Mode",
|
||||
"Random": "Random Concatenation (Recommended)",
|
||||
"Sequential": "Sequential Concatenation",
|
||||
"Video Ratio": "Video Aspect Ratio",
|
||||
"Portrait": "Portrait 9:16",
|
||||
"Landscape": "Landscape 16:9",
|
||||
"Clip Duration": "Maximum Duration of Video Clips (seconds)",
|
||||
"Number of Videos Generated Simultaneously": "Number of Videos Generated Simultaneously",
|
||||
"Audio Settings": "**Audio Settings**",
|
||||
"Speech Synthesis": "Speech Synthesis Voice",
|
||||
"Male": "Male",
|
||||
"Female": "Female",
|
||||
"Background Music": "Background Music",
|
||||
"No Background Music": "No Background Music",
|
||||
"Random Background Music": "Random Background Music",
|
||||
"Custom Background Music": "Custom Background Music",
|
||||
"Custom Background Music File": "Please enter the file path for custom background music:",
|
||||
"Background Music Volume": "Background Music Volume (0.2 represents 20%, background music should not be too loud)",
|
||||
"Subtitle Settings": "**Subtitle Settings**",
|
||||
"Enable Subtitles": "Enable Subtitles (If unchecked, the settings below will not take effect)",
|
||||
"Font": "Subtitle Font",
|
||||
"Position": "Subtitle Position",
|
||||
"Top": "Top",
|
||||
"Center": "Center",
|
||||
"Bottom": "Bottom (Recommended)",
|
||||
"Font Size": "Subtitle Font Size",
|
||||
"Font Color": "Subtitle Font Color",
|
||||
"Stroke Color": "Subtitle Outline Color",
|
||||
"Stroke Width": "Subtitle Outline Width",
|
||||
"Generate Video": "Generate Video",
|
||||
"Video Script and Subject Cannot Both Be Empty": "Video Subject and Video Script cannot both be empty",
|
||||
"Generating Video": "Generating video, please wait...",
|
||||
"Start Generating Video": "Start Generating Video",
|
||||
"Video Generation Completed": "Video Generation Completed"
|
||||
}
|
||||
}
|
||||
51
webui/i18n/zh.json
Normal file
51
webui/i18n/zh.json
Normal file
@ -0,0 +1,51 @@
|
||||
{
|
||||
"Language": "简体中文",
|
||||
"Translation": {
|
||||
"Video Script Settings": "**文案设置**",
|
||||
"Video Subject": "视频主题(给定一个关键词,:red[AI自动生成]视频文案)",
|
||||
"Script Language": "生成视频脚本的语言(一般情况AI会自动根据你输入的主题语言输出)",
|
||||
"Generate Video Script and Keywords": "点击使用AI根据**主题**生成 【视频文案】 和 【视频关键词】",
|
||||
"Auto Detect": "自动检测",
|
||||
"Video Script": "视频文案(:blue[①可不填,使用AI生成 ②合理使用标点断句,有助于生成字幕])",
|
||||
"Generate Video Keywords": "点击使用AI根据**文案**生成【视频关键词】",
|
||||
"Please Enter the Video Subject": "请先填写视频文案",
|
||||
"Generating Video Script and Keywords": "AI正在生成视频文案和关键词...",
|
||||
"Generating Video Keywords": "AI正在生成视频关键词...",
|
||||
"Video Keywords": "视频关键词(:blue[①可不填,使用AI生成 ②用**英文逗号**分隔,只支持英文])",
|
||||
"Video Settings": "**视频设置**",
|
||||
"Video Concat Mode": "视频拼接模式",
|
||||
"Random": "随机拼接(推荐)",
|
||||
"Sequential": "顺序拼接",
|
||||
"Video Ratio": "视频比例",
|
||||
"Portrait": "竖屏 9:16(抖音视频)",
|
||||
"Landscape": "横屏 16:9(西瓜视频)",
|
||||
"Clip Duration": "视频片段最大时长(秒)",
|
||||
"Number of Videos Generated Simultaneously": "同时生成视频数量",
|
||||
"Audio Settings": "**音频设置**",
|
||||
"Speech Synthesis": "朗读声音",
|
||||
"Male": "男性",
|
||||
"Female": "女性",
|
||||
"Background Music": "背景音乐",
|
||||
"No Background Music": "无背景音乐",
|
||||
"Random Background Music": "随机背景音乐",
|
||||
"Custom Background Music": "自定义背景音乐",
|
||||
"Custom Background Music File": "请输入自定义背景音乐的文件路径",
|
||||
"Background Music Volume": "背景音乐音量(0.2表示20%,背景声音不宜过高)",
|
||||
"Subtitle Settings": "**字幕设置**",
|
||||
"Enable Subtitles": "启用字幕(若取消勾选,下面的设置都将不生效)",
|
||||
"Font": "字幕字体",
|
||||
"Position": "字幕位置",
|
||||
"Top": "顶部",
|
||||
"Center": "中间",
|
||||
"Bottom": "底部(推荐)",
|
||||
"Font Size": "字幕大小",
|
||||
"Font Color": "字幕颜色",
|
||||
"Stroke Color": "描边颜色",
|
||||
"Stroke Width": "描边粗细",
|
||||
"Generate Video": "生成视频",
|
||||
"Video Script and Subject Cannot Both Be Empty": "视频主题 和 视频文案,不能同时为空",
|
||||
"Generating Video": "正在生成视频,请稍候...",
|
||||
"Start Generating Video": "开始生成视频",
|
||||
"Video Generation Completed": "视频生成完成"
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user