The response has been limited to 50k tokens of the smallest files in the repo. You can remove this limitation by removing the max tokens filter.
├── .dockerignore
├── .github
    └── ISSUE_TEMPLATE
    │   ├── bug_report.yml
    │   ├── config.yml
    │   └── feature_request.yml
├── .gitignore
├── Dockerfile
├── LICENSE
├── README-en.md
├── README.md
├── app
    ├── __init__.py
    ├── asgi.py
    ├── config
    │   ├── __init__.py
    │   └── config.py
    ├── controllers
    │   ├── base.py
    │   ├── manager
    │   │   ├── base_manager.py
    │   │   ├── memory_manager.py
    │   │   └── redis_manager.py
    │   ├── ping.py
    │   └── v1
    │   │   ├── base.py
    │   │   ├── llm.py
    │   │   └── video.py
    ├── models
    │   ├── __init__.py
    │   ├── const.py
    │   ├── exception.py
    │   └── schema.py
    ├── router.py
    ├── services
    │   ├── __init__.py
    │   ├── llm.py
    │   ├── material.py
    │   ├── state.py
    │   ├── subtitle.py
    │   ├── task.py
    │   ├── utils
    │   │   └── video_effects.py
    │   ├── video.py
    │   └── voice.py
    └── utils
    │   └── utils.py
├── config.example.toml
├── docker-compose.yml
├── docs
    ├── MoneyPrinterTurbo.ipynb
    ├── api.jpg
    ├── picwish.com.jpg
    ├── picwish.jpg
    ├── reccloud.cn.jpg
    ├── reccloud.com.jpg
    ├── voice-list.txt
    ├── webui-en.jpg
    └── webui.jpg
├── main.py
├── requirements.txt
├── resource
    ├── fonts
    │   ├── Charm-Bold.ttf
    │   ├── Charm-Regular.ttf
    │   ├── MicrosoftYaHeiBold.ttc
    │   ├── MicrosoftYaHeiNormal.ttc
    │   ├── STHeitiLight.ttc
    │   ├── STHeitiMedium.ttc
    │   └── UTM Kabel KT.ttf
    ├── public
    │   └── index.html
    └── songs
    │   ├── output000.mp3
    │   ├── output001.mp3
    │   ├── output002.mp3
    │   ├── output003.mp3
    │   ├── output004.mp3
    │   ├── output005.mp3
    │   ├── output006.mp3
    │   ├── output007.mp3
    │   ├── output008.mp3
    │   ├── output009.mp3
    │   ├── output010.mp3
    │   ├── output011.mp3
    │   ├── output012.mp3
    │   ├── output013.mp3
    │   ├── output014.mp3
    │   ├── output015.mp3
    │   ├── output016.mp3
    │   ├── output017.mp3
    │   ├── output018.mp3
    │   ├── output019.mp3
    │   ├── output020.mp3
    │   ├── output021.mp3
    │   ├── output022.mp3
    │   ├── output023.mp3
    │   ├── output024.mp3
    │   ├── output025.mp3
    │   ├── output027.mp3
    │   ├── output028.mp3
    │   └── output029.mp3
├── test
    ├── README.md
    ├── __init__.py
    ├── resources
    │   ├── 1.png
    │   ├── 1.png.mp4
    │   ├── 2.png
    │   ├── 2.png.mp4
    │   ├── 3.png
    │   ├── 3.png.mp4
    │   ├── 4.png
    │   ├── 5.png
    │   ├── 6.png
    │   ├── 7.png
    │   ├── 8.png
    │   └── 9.png
    └── services
    │   ├── __init__.py
    │   ├── test_task.py
    │   ├── test_video.py
    │   └── test_voice.py
├── webui.bat
├── webui.sh
└── webui
    ├── .streamlit
        └── config.toml
    ├── Main.py
    └── i18n
        ├── de.json
        ├── en.json
        ├── pt.json
        ├── vi.json
        └── zh.json


/.dockerignore:
--------------------------------------------------------------------------------
 1 | # Exclude common Python files and directories
 2 | venv/
 3 | __pycache__/
 4 | *.pyc
 5 | *.pyo
 6 | *.pyd
 7 | *.pyz
 8 | *.pyw
 9 | *.pyi
10 | *.egg-info/
11 | 
12 | # Exclude development and local files
13 | .env
14 | .env.*
15 | *.log
16 | *.db
17 | 
18 | # Exclude version control system files
19 | .git/
20 | .gitignore
21 | .svn/
22 | 
23 | storage/
24 | config.toml
25 | 


--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/bug_report.yml:
--------------------------------------------------------------------------------
 1 | name: 🐛 Bug | Bug Report
 2 | description: 报告错误或异常问题 | Report an error or unexpected behavior
 3 | title: "[Bug]: "
 4 | labels:
 5 |   - bug
 6 | 
 7 | body:
 8 |   - type: markdown
 9 |     attributes:
10 |       value: |
11 |         **提交问题前,请确保您已阅读以下文档:[Getting Started (English)](https://github.com/harry0703/MoneyPrinterTurbo/blob/main/README-en.md#system-requirements-) 或 [快速开始 (中文)](https://github.com/harry0703/MoneyPrinterTurbo/blob/main/README.md#%E5%BF%AB%E9%80%9F%E5%BC%80%E5%A7%8B-)。**
12 |         
13 |         **Before submitting an issue, please make sure you've read the following documentation: [Getting Started (English)](https://github.com/harry0703/MoneyPrinterTurbo/blob/main/README-en.md#system-requirements-) or [快速开始 (Chinese)](https://github.com/harry0703/MoneyPrinterTurbo/blob/main/README.md#%E5%BF%AB%E9%80%9F%E5%BC%80%E5%A7%8B-).**
14 |         
15 |   - type: textarea
16 |     attributes:
17 |       label: 问题描述 | Current Behavior
18 |       description: |
19 |         描述您遇到的问题
20 |         Describe the issue you're experiencing
21 |       placeholder: |
22 |         当我执行...操作时,程序出现了...问题
23 |         When I perform..., the program shows...
24 |     validations:
25 |       required: true
26 |   - type: textarea
27 |     attributes:
28 |       label: 重现步骤 | Steps to Reproduce
29 |       description: |
30 |         详细描述如何重现此问题
31 |         Describe in detail how to reproduce this issue
32 |       placeholder: |
33 |         1. 打开...
34 |         2. 点击...
35 |         3. 出现错误...
36 |         
37 |         1. Open...
38 |         2. Click on...
39 |         3. Error occurs...
40 |     validations:
41 |       required: true
42 |   - type: textarea
43 |     attributes:
44 |       label: 错误日志 | Error Logs
45 |       description: |
46 |         请提供相关错误信息或日志(注意不要包含敏感信息)
47 |         Please provide any error messages or logs (be careful not to include sensitive information)
48 |       placeholder: |
49 |         错误信息、日志或截图...
50 |         Error messages, logs, or screenshots...
51 |     validations:
52 |       required: true
53 |   - type: input
54 |     attributes:
55 |       label: Python 版本 | Python Version
56 |       description: |
57 |         您使用的 Python 版本
58 |         The Python version you're using
59 |       placeholder: v3.13.0, v3.10.0, etc.
60 |     validations:
61 |       required: true
62 |   - type: input
63 |     attributes:
64 |       label: 操作系统 | Operating System
65 |       description: |
66 |         您的操作系统信息
67 |         Your operating system information
68 |       placeholder: macOS 14.1, Windows 11, Ubuntu 22.04, etc.
69 |     validations:
70 |       required: true
71 |   - type: input
72 |     attributes:
73 |       label: MoneyPrinterTurbo 版本 | Version
74 |       description: |
75 |         您使用的 MoneyPrinterTurbo 版本
76 |         The version of MoneyPrinterTurbo you're using
77 |       placeholder: v1.2.2, etc.
78 |     validations:
79 |       required: true
80 |   - type: textarea
81 |     attributes:
82 |       label: 补充信息 | Additional Information
83 |       description: |
84 |         其他对解决问题有帮助的信息(如截图、视频等)
85 |         Any other information that might help solve the issue (screenshots, videos, etc.)
86 |     validations:
87 |       required: false


--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/config.yml:
--------------------------------------------------------------------------------
1 | blank_issues_enabled: false


--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/feature_request.yml:
--------------------------------------------------------------------------------
 1 | name: ✨ 增加功能 | Feature Request
 2 | description: 为此项目提出一个新想法或建议 | Suggest a new idea for this project
 3 | title: "[Feature]: "
 4 | labels:
 5 |   - enhancement
 6 | 
 7 | body:
 8 |   - type: textarea
 9 |     attributes:
10 |       label: 需求描述 | Problem Statement
11 |       description: |
12 |         请描述您希望解决的问题或需求 
13 |         Please describe the problem you want to solve
14 |       placeholder: |
15 |         我在使用过程中遇到了...
16 |         I encountered... when using this project
17 |     validations:
18 |       required: true
19 |   - type: textarea
20 |     attributes:
21 |       label: 建议的解决方案 | Proposed Solution
22 |       description: |
23 |         请描述您认为可行的解决方案或实现方式
24 |         Please describe your suggested solution or implementation
25 |       placeholder: |
26 |         可以考虑添加...功能来解决这个问题
27 |         Consider adding... feature to address this issue
28 |     validations:
29 |       required: true


--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
 1 | .DS_Store
 2 | /config.toml
 3 | /storage/
 4 | /.idea/
 5 | /app/services/__pycache__
 6 | /app/__pycache__/
 7 | /app/config/__pycache__/
 8 | /app/models/__pycache__/
 9 | /app/utils/__pycache__/
10 | /*/__pycache__/*
11 | .vscode
12 | /**/.streamlit
13 | __pycache__
14 | logs/
15 | 
16 | node_modules
17 | # VuePress 默认临时文件目录
18 | /sites/docs/.vuepress/.temp
19 | # VuePress 默认缓存目录
20 | /sites/docs/.vuepress/.cache
21 | # VuePress 默认构建生成的静态文件目录
22 | /sites/docs/.vuepress/dist
23 | # 模型目录
24 | /models/
25 | ./models/*
26 | 
27 | venv/
28 | .venv


--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
 1 | # Use an official Python runtime as a parent image
 2 | FROM python:3.11-slim-bullseye
 3 | 
 4 | # Set the working directory in the container
 5 | WORKDIR /MoneyPrinterTurbo
 6 | 
 7 | # 设置/MoneyPrinterTurbo目录权限为777
 8 | RUN chmod 777 /MoneyPrinterTurbo
 9 | 
10 | ENV PYTHONPATH="/MoneyPrinterTurbo"
11 | 
12 | # Install system dependencies
13 | RUN apt-get update && apt-get install -y \
14 |     git \
15 |     imagemagick \
16 |     ffmpeg \
17 |     && rm -rf /var/lib/apt/lists/*
18 | 
19 | # Fix security policy for ImageMagick
20 | RUN sed -i '/<policy domain="path" rights="none" pattern="@\*"/d' /etc/ImageMagick-6/policy.xml
21 | 
22 | # Copy only the requirements.txt first to leverage Docker cache
23 | COPY requirements.txt ./
24 | 
25 | # Install Python dependencies
26 | RUN pip install --no-cache-dir -r requirements.txt
27 | 
28 | # Now copy the rest of the codebase into the image
29 | COPY . .
30 | 
31 | # Expose the port the app runs on
32 | EXPOSE 8501
33 | 
34 | # Command to run the application
35 | CMD ["streamlit", "run", "./webui/Main.py","--browser.serverAddress=127.0.0.1","--server.enableCORS=True","--browser.gatherUsageStats=False"]
36 | 
37 | # 1. Build the Docker image using the following command
38 | # docker build -t moneyprinterturbo .
39 | 
40 | # 2. Run the Docker container using the following command
41 | ## For Linux or MacOS:
42 | # docker run -v $(pwd)/config.toml:/MoneyPrinterTurbo/config.toml -v $(pwd)/storage:/MoneyPrinterTurbo/storage -p 8501:8501 moneyprinterturbo
43 | ## For Windows:
44 | # docker run -v ${PWD}/config.toml:/MoneyPrinterTurbo/config.toml -v ${PWD}/storage:/MoneyPrinterTurbo/storage -p 8501:8501 moneyprinterturbo


--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
 1 | MIT License
 2 | 
 3 | Copyright (c) 2024 Harry
 4 | 
 5 | Permission is hereby granted, free of charge, to any person obtaining a copy
 6 | of this software and associated documentation files (the "Software"), to deal
 7 | in the Software without restriction, including without limitation the rights
 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 | 
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 | 
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.


--------------------------------------------------------------------------------
/README-en.md:
--------------------------------------------------------------------------------
  1 | <div align="center">
  2 | <h1 align="center">MoneyPrinterTurbo 💸</h1>
  3 | 
  4 | <p align="center">
  5 |   <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>
  6 |   <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>
  7 |   <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>
  8 |   <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>
  9 | </p>
 10 | 
 11 | <h3>English | <a href="README.md">简体中文</a></h3>
 12 | 
 13 | <div align="center">
 14 |   <a href="https://trendshift.io/repositories/8731" target="_blank"><img src="https://trendshift.io/api/badge/repositories/8731" alt="harry0703%2FMoneyPrinterTurbo | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a>
 15 | </div>
 16 | 
 17 | Simply provide a <b>topic</b> or <b>keyword</b> for a video, and it will automatically generate the video copy, video
 18 | materials, video subtitles, and video background music before synthesizing a high-definition short video.
 19 | 
 20 | ### WebUI
 21 | 
 22 | ![](docs/webui-en.jpg)
 23 | 
 24 | ### API Interface
 25 | 
 26 | ![](docs/api.jpg)
 27 | 
 28 | </div>
 29 | 
 30 | ## Special Thanks 🙏
 31 | 
 32 | Due to the **deployment** and **usage** of this project, there is a certain threshold for some beginner users. We would
 33 | like to express our special thanks to
 34 | 
 35 | **RecCloud (AI-Powered Multimedia Service Platform)** for providing a free `AI Video Generator` service based on this
 36 | project. It allows for online use without deployment, which is very convenient.
 37 | 
 38 | - Chinese version: https://reccloud.cn
 39 | - English version: https://reccloud.com
 40 | 
 41 | ![](docs/reccloud.com.jpg)
 42 | 
 43 | ## Thanks for Sponsorship 🙏
 44 | 
 45 | Thanks to Picwish https://picwish.com for supporting and sponsoring this project, enabling continuous updates and maintenance.
 46 | 
 47 | Picwish focuses on the **image processing field**, providing a rich set of **image processing tools** that extremely simplify complex operations, truly making image processing easier.
 48 | 
 49 | ![picwish.jpg](docs/picwish.com.jpg)
 50 | 
 51 | ## Features 🎯
 52 | 
 53 | - [x] Complete **MVC architecture**, **clearly structured** code, easy to maintain, supports both `API`
 54 |   and `Web interface`
 55 | - [x] Supports **AI-generated** video copy, as well as **customized copy**
 56 | - [x] Supports various **high-definition video** sizes
 57 |     - [x] Portrait 9:16, `1080x1920`
 58 |     - [x] Landscape 16:9, `1920x1080`
 59 | - [x] Supports **batch video generation**, allowing the creation of multiple videos at once, then selecting the most
 60 |   satisfactory one
 61 | - [x] Supports setting the **duration of video clips**, facilitating adjustments to material switching frequency
 62 | - [x] Supports video copy in both **Chinese** and **English**
 63 | - [x] Supports **multiple voice** synthesis, with **real-time preview** of effects
 64 | - [x] Supports **subtitle generation**, with adjustable `font`, `position`, `color`, `size`, and also
 65 |   supports `subtitle outlining`
 66 | - [x] Supports **background music**, either random or specified music files, with adjustable `background music volume`
 67 | - [x] Video material sources are **high-definition** and **royalty-free**, and you can also use your own **local materials**
 68 | - [x] Supports integration with various models such as **OpenAI**, **Moonshot**, **Azure**, **gpt4free**, **one-api**, **Qwen**, **Google Gemini**, **Ollama**, **DeepSeek**, **ERNIE**, **Pollinations** and more
 69 | 
 70 | ### Future Plans 📅
 71 | 
 72 | - [ ] GPT-SoVITS dubbing support
 73 | - [ ] Optimize voice synthesis using large models for more natural and emotionally rich voice output
 74 | - [ ] Add video transition effects for a smoother viewing experience
 75 | - [ ] Add more video material sources, improve the matching between video materials and script
 76 | - [ ] Add video length options: short, medium, long
 77 | - [ ] Support more voice synthesis providers, such as OpenAI TTS
 78 | - [ ] Automate upload to YouTube platform
 79 | 
 80 | ## Video Demos 📺
 81 | 
 82 | ### Portrait 9:16
 83 | 
 84 | <table>
 85 | <thead>
 86 | <tr>
 87 | <th align="center"><g-emoji class="g-emoji" alias="arrow_forward">▶️</g-emoji> How to Add Fun to Your Life </th>
 88 | <th align="center"><g-emoji class="g-emoji" alias="arrow_forward">▶️</g-emoji> What is the Meaning of Life</th>
 89 | </tr>
 90 | </thead>
 91 | <tbody>
 92 | <tr>
 93 | <td align="center"><video src="https://github.com/harry0703/MoneyPrinterTurbo/assets/4928832/a84d33d5-27a2-4aba-8fd0-9fb2bd91c6a6"></video></td>
 94 | <td align="center"><video src="https://github.com/harry0703/MoneyPrinterTurbo/assets/4928832/112c9564-d52b-4472-99ad-970b75f66476"></video></td>
 95 | </tr>
 96 | </tbody>
 97 | </table>
 98 | 
 99 | ### Landscape 16:9
100 | 
101 | <table>
102 | <thead>
103 | <tr>
104 | <th align="center"><g-emoji class="g-emoji" alias="arrow_forward">▶️</g-emoji> What is the Meaning of Life</th>
105 | <th align="center"><g-emoji class="g-emoji" alias="arrow_forward">▶️</g-emoji> Why Exercise</th>
106 | </tr>
107 | </thead>
108 | <tbody>
109 | <tr>
110 | <td align="center"><video src="https://github.com/harry0703/MoneyPrinterTurbo/assets/4928832/346ebb15-c55f-47a9-a653-114f08bb8073"></video></td>
111 | <td align="center"><video src="https://github.com/harry0703/MoneyPrinterTurbo/assets/4928832/271f2fae-8283-44a0-8aa0-0ed8f9a6fa87"></video></td>
112 | </tr>
113 | </tbody>
114 | </table>
115 | 
116 | ## System Requirements 📦
117 | 
118 | - Recommended minimum 4 CPU cores or more, 4G of memory or more, GPU is not required
119 | - Windows 10 or MacOS 11.0, and their later versions
120 | 
121 | ## Quick Start 🚀
122 | 
123 | ### Run in Google Colab 
124 | Want to try MoneyPrinterTurbo without setting up a local environment? Run it directly in Google Colab!
125 | 
126 | [![Open in Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/harry0703/MoneyPrinterTurbo/blob/main/docs/MoneyPrinterTurbo.ipynb)
127 | 
128 | 
129 | ### Windows
130 | 
131 | Google Drive (v1.2.6): https://drive.google.com/file/d/1HsbzfT7XunkrCrHw5ncUjFX8XX4zAuUh/view?usp=sharing
132 | 
133 | After downloading, it is recommended to **double-click** `update.bat` first to update to the **latest code**, then double-click `start.bat` to launch
134 | 
135 | After launching, the browser will open automatically (if it opens blank, it is recommended to use **Chrome** or **Edge**)
136 | 
137 | ### Other Systems
138 | 
139 | One-click startup packages have not been created yet. See the **Installation & Deployment** section below. It is recommended to use **docker** for deployment, which is more convenient.
140 | 
141 | ## Installation & Deployment 📥
142 | 
143 | ### Prerequisites
144 | 
145 | #### ① Clone the Project
146 | 
147 | ```shell
148 | git clone https://github.com/harry0703/MoneyPrinterTurbo.git
149 | ```
150 | 
151 | #### ② Modify the Configuration File
152 | 
153 | - Copy the `config.example.toml` file and rename it to `config.toml`
154 | - Follow the instructions in the `config.toml` file to configure `pexels_api_keys` and `llm_provider`, and according to
155 |   the llm_provider's service provider, set up the corresponding API Key
156 | 
157 | ### Docker Deployment 🐳
158 | 
159 | #### ① Launch the Docker Container
160 | 
161 | If you haven't installed Docker, please install it first https://www.docker.com/products/docker-desktop/
162 | If you are using a Windows system, please refer to Microsoft's documentation:
163 | 
164 | 1. https://learn.microsoft.com/en-us/windows/wsl/install
165 | 2. https://learn.microsoft.com/en-us/windows/wsl/tutorials/wsl-containers
166 | 
167 | ```shell
168 | cd MoneyPrinterTurbo
169 | docker-compose up
170 | ```
171 | 
172 | > Note:The latest version of docker will automatically install docker compose in the form of a plug-in, and the start command is adjusted to `docker compose up `
173 | 
174 | #### ② Access the Web Interface
175 | 
176 | Open your browser and visit http://0.0.0.0:8501
177 | 
178 | #### ③ Access the API Interface
179 | 
180 | Open your browser and visit http://0.0.0.0:8080/docs Or http://0.0.0.0:8080/redoc
181 | 
182 | ### Manual Deployment 📦
183 | 
184 | #### ① Create a Python Virtual Environment
185 | 
186 | It is recommended to create a Python virtual environment using [conda](https://conda.io/projects/conda/en/latest/user-guide/install/index.html)
187 | 
188 | ```shell
189 | git clone https://github.com/harry0703/MoneyPrinterTurbo.git
190 | cd MoneyPrinterTurbo
191 | conda create -n MoneyPrinterTurbo python=3.11
192 | conda activate MoneyPrinterTurbo
193 | pip install -r requirements.txt
194 | ```
195 | 
196 | #### ② Install ImageMagick
197 | 
198 | ###### Windows:
199 | 
200 | - Download https://imagemagick.org/script/download.php Choose the Windows version, make sure to select the **static library** version, such as ImageMagick-7.1.1-32-Q16-x64-**static**.exe
201 | - Install the downloaded ImageMagick, **do not change the installation path**
202 | - Modify the `config.toml` configuration file, set `imagemagick_path` to your actual installation path
203 | 
204 | ###### MacOS:
205 | 
206 | ```shell
207 | brew install imagemagick
208 | ````
209 | 
210 | ###### Ubuntu
211 | 
212 | ```shell
213 | sudo apt-get install imagemagick
214 | ```
215 | 
216 | ###### CentOS
217 | 
218 | ```shell
219 | sudo yum install ImageMagick
220 | ```
221 | 
222 | #### ③ Launch the Web Interface 🌐
223 | 
224 | Note that you need to execute the following commands in the `root directory` of the MoneyPrinterTurbo project
225 | 
226 | ###### Windows
227 | 
228 | ```bat
229 | webui.bat
230 | ```
231 | 
232 | ###### MacOS or Linux
233 | 
234 | ```shell
235 | sh webui.sh
236 | ```
237 | 
238 | After launching, the browser will open automatically
239 | 
240 | #### ④ Launch the API Service 🚀
241 | 
242 | ```shell
243 | python main.py
244 | ```
245 | 
246 | After launching, you can view the `API documentation` at http://127.0.0.1:8080/docs and directly test the interface
247 | online for a quick experience.
248 | 
249 | ## Voice Synthesis 🗣
250 | 
251 | A list of all supported voices can be viewed here: [Voice List](./docs/voice-list.txt)
252 | 
253 | 2024-04-16 v1.1.2 Added 9 new Azure voice synthesis voices that require API KEY configuration. These voices sound more realistic.
254 | 
255 | ## Subtitle Generation 📜
256 | 
257 | Currently, there are 2 ways to generate subtitles:
258 | 
259 | - **edge**: Faster generation speed, better performance, no specific requirements for computer configuration, but the
260 |   quality may be unstable
261 | - **whisper**: Slower generation speed, poorer performance, specific requirements for computer configuration, but more
262 |   reliable quality
263 | 
264 | You can switch between them by modifying the `subtitle_provider` in the `config.toml` configuration file
265 | 
266 | It is recommended to use `edge` mode, and switch to `whisper` mode if the quality of the subtitles generated is not
267 | satisfactory.
268 | 
269 | > Note:
270 | >
271 | > 1. In whisper mode, you need to download a model file from HuggingFace, about 3GB in size, please ensure good internet connectivity
272 | > 2. If left blank, it means no subtitles will be generated.
273 | 
274 | > Since HuggingFace is not accessible in China, you can use the following methods to download the `whisper-large-v3` model file
275 | 
276 | Download links:
277 | 
278 | - Baidu Netdisk: https://pan.baidu.com/s/11h3Q6tsDtjQKTjUu3sc5cA?pwd=xjs9
279 | - Quark Netdisk: https://pan.quark.cn/s/3ee3d991d64b
280 | 
281 | After downloading the model, extract it and place the entire directory in `.\MoneyPrinterTurbo\models`,
282 | The final file path should look like this: `.\MoneyPrinterTurbo\models\whisper-large-v3`
283 | 
284 | ```
285 | MoneyPrinterTurbo
286 |   ├─models
287 |   │   └─whisper-large-v3
288 |   │          config.json
289 |   │          model.bin
290 |   │          preprocessor_config.json
291 |   │          tokenizer.json
292 |   │          vocabulary.json
293 | ```
294 | 
295 | ## Background Music 🎵
296 | 
297 | Background music for videos is located in the project's `resource/songs` directory.
298 | > The current project includes some default music from YouTube videos. If there are copyright issues, please delete
299 | > them.
300 | 
301 | ## Subtitle Fonts 🅰
302 | 
303 | Fonts for rendering video subtitles are located in the project's `resource/fonts` directory, and you can also add your
304 | own fonts.
305 | 
306 | ## Common Questions 🤔
307 | 
308 | ### ❓RuntimeError: No ffmpeg exe could be found
309 | 
310 | Normally, ffmpeg will be automatically downloaded and detected.
311 | However, if your environment has issues preventing automatic downloads, you may encounter the following error:
312 | 
313 | ```
314 | RuntimeError: No ffmpeg exe could be found.
315 | Install ffmpeg on your system, or set the IMAGEIO_FFMPEG_EXE environment variable.
316 | ```
317 | 
318 | In this case, you can download ffmpeg from https://www.gyan.dev/ffmpeg/builds/, unzip it, and set `ffmpeg_path` to your
319 | actual installation path.
320 | 
321 | ```toml
322 | [app]
323 | # Please set according to your actual path, note that Windows path separators are \\
324 | ffmpeg_path = "C:\\Users\\harry\\Downloads\\ffmpeg.exe"
325 | ```
326 | 
327 | ### ❓ImageMagick is not installed on your computer
328 | 
329 | [issue 33](https://github.com/harry0703/MoneyPrinterTurbo/issues/33)
330 | 
331 | 1. Follow the `example configuration` provided `download address` to
332 |    install https://imagemagick.org/archive/binaries/ImageMagick-7.1.1-30-Q16-x64-static.exe, using the static library
333 | 2. Do not install in a path with Chinese characters to avoid unpredictable issues
334 | 
335 | [issue 54](https://github.com/harry0703/MoneyPrinterTurbo/issues/54#issuecomment-2017842022)
336 | 
337 | For Linux systems, you can manually install it, refer to https://cn.linux-console.net/?p=16978
338 | 
339 | Thanks to [@wangwenqiao666](https://github.com/wangwenqiao666) for their research and exploration
340 | 
341 | ### ❓ImageMagick's security policy prevents operations related to temporary file @/tmp/tmpur5hyyto.txt
342 | 
343 | You can find these policies in ImageMagick's configuration file policy.xml.
344 | This file is usually located in /etc/ImageMagick-`X`/ or a similar location in the ImageMagick installation directory.
345 | Modify the entry containing `pattern="@"`, change `rights="none"` to `rights="read|write"` to allow read and write operations on files.
346 | 
347 | ### ❓OSError: [Errno 24] Too many open files
348 | 
349 | This issue is caused by the system's limit on the number of open files. You can solve it by modifying the system's file open limit.
350 | 
351 | Check the current limit:
352 | 
353 | ```shell
354 | ulimit -n
355 | ```
356 | 
357 | If it's too low, you can increase it, for example:
358 | 
359 | ```shell
360 | ulimit -n 10240
361 | ```
362 | 
363 | ### ❓Whisper model download failed, with the following error
364 | 
365 | LocalEntryNotfoundEror: Cannot find an appropriate cached snapshotfolderfor the specified revision on the local disk and
366 | outgoing trafic has been disabled.
367 | To enablerepo look-ups and downloads online, pass 'local files only=False' as input.
368 | 
369 | or
370 | 
371 | An error occured while synchronizing the model Systran/faster-whisper-large-v3 from the Hugging Face Hub:
372 | An error happened while trying to locate the files on the Hub and we cannot find the appropriate snapshot folder for the
373 | specified revision on the local disk. Please check your internet connection and try again.
374 | Trying to load the model directly from the local cache, if it exists.
375 | 
376 | Solution: [Click to see how to manually download the model from netdisk](#subtitle-generation-)
377 | 
378 | ## Feedback & Suggestions 📢
379 | 
380 | - You can submit an [issue](https://github.com/harry0703/MoneyPrinterTurbo/issues) or
381 |   a [pull request](https://github.com/harry0703/MoneyPrinterTurbo/pulls).
382 | 
383 | ## License 📝
384 | 
385 | Click to view the [`LICENSE`](LICENSE) file
386 | 
387 | ## Star History
388 | 
389 | [![Star History Chart](https://api.star-history.com/svg?repos=harry0703/MoneyPrinterTurbo&type=Date)](https://star-history.com/#harry0703/MoneyPrinterTurbo&Date)
390 | 


--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
  1 | <div align="center">
  2 | <h1 align="center">MoneyPrinterTurbo 💸</h1>
  3 | 
  4 | <p align="center">
  5 |   <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>
  6 |   <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>
  7 |   <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>
  8 |   <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>
  9 | </p>
 10 | <br>
 11 | <h3>简体中文 | <a href="README-en.md">English</a></h3>
 12 | <div align="center">
 13 |   <a href="https://trendshift.io/repositories/8731" target="_blank"><img src="https://trendshift.io/api/badge/repositories/8731" alt="harry0703%2FMoneyPrinterTurbo | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a>
 14 | </div>
 15 | <br>
 16 | 只需提供一个视频 <b>主题</b> 或 <b>关键词</b> ,就可以全自动生成视频文案、视频素材、视频字幕、视频背景音乐,然后合成一个高清的短视频。
 17 | <br>
 18 | 
 19 | <h4>Web界面</h4>
 20 | 
 21 | ![](docs/webui.jpg)
 22 | 
 23 | <h4>API界面</h4>
 24 | 
 25 | ![](docs/api.jpg)
 26 | 
 27 | </div>
 28 | 
 29 | ## 特别感谢 🙏
 30 | 
 31 | 由于该项目的 **部署** 和 **使用**,对于一些小白用户来说,还是 **有一定的门槛**,在此特别感谢
 32 | **录咖(AI智能 多媒体服务平台)** 网站基于该项目,提供的免费`AI视频生成器`服务,可以不用部署,直接在线使用,非常方便。
 33 | 
 34 | - 中文版:https://reccloud.cn
 35 | - 英文版:https://reccloud.com
 36 | 
 37 | ![](docs/reccloud.cn.jpg)
 38 | 
 39 | ## 感谢赞助 🙏
 40 | 
 41 | 感谢佐糖 https://picwish.cn 对该项目的支持和赞助,使得该项目能够持续的更新和维护。
 42 | 
 43 | 佐糖专注于**图像处理领域**,提供丰富的**图像处理工具**,将复杂操作极致简化,真正实现让图像处理更简单。
 44 | 
 45 | ![picwish.jpg](docs/picwish.jpg)
 46 | 
 47 | ## 功能特性 🎯
 48 | 
 49 | - [x] 完整的 **MVC架构**,代码 **结构清晰**,易于维护,支持 `API` 和 `Web界面`
 50 | - [x] 支持视频文案 **AI自动生成**,也可以**自定义文案**
 51 | - [x] 支持多种 **高清视频** 尺寸
 52 |     - [x] 竖屏 9:16,`1080x1920`
 53 |     - [x] 横屏 16:9,`1920x1080`
 54 | - [x] 支持 **批量视频生成**,可以一次生成多个视频,然后选择一个最满意的
 55 | - [x] 支持 **视频片段时长** 设置,方便调节素材切换频率
 56 | - [x] 支持 **中文** 和 **英文** 视频文案
 57 | - [x] 支持 **多种语音** 合成,可 **实时试听** 效果
 58 | - [x] 支持 **字幕生成**,可以调整 `字体`、`位置`、`颜色`、`大小`,同时支持`字幕描边`设置
 59 | - [x] 支持 **背景音乐**,随机或者指定音乐文件,可设置`背景音乐音量`
 60 | - [x] 视频素材来源 **高清**,而且 **无版权**,也可以使用自己的 **本地素材**
 61 | - [x] 支持 **OpenAI**、**Moonshot**、**Azure**、**gpt4free**、**one-api**、**通义千问**、**Google Gemini**、**Ollama**、**DeepSeek**、 **文心一言**, **Pollinations** 等多种模型接入
 62 |     - 中国用户建议使用 **DeepSeek** 或 **Moonshot** 作为大模型提供商(国内可直接访问,不需要VPN。注册就送额度,基本够用)
 63 | 
 64 | 
 65 | ### 后期计划 📅
 66 | 
 67 | - [ ] GPT-SoVITS 配音支持
 68 | - [ ] 优化语音合成,利用大模型,使其合成的声音,更加自然,情绪更加丰富
 69 | - [ ] 增加视频转场效果,使其看起来更加的流畅
 70 | - [ ] 增加更多视频素材来源,优化视频素材和文案的匹配度
 71 | - [ ] 增加视频长度选项:短、中、长
 72 | - [ ] 支持更多的语音合成服务商,比如 OpenAI TTS
 73 | - [ ] 自动上传到YouTube平台
 74 | 
 75 | ## 视频演示 📺
 76 | 
 77 | ### 竖屏 9:16
 78 | 
 79 | <table>
 80 | <thead>
 81 | <tr>
 82 | <th align="center"><g-emoji class="g-emoji" alias="arrow_forward">▶️</g-emoji> 《如何增加生活的乐趣》</th>
 83 | <th align="center"><g-emoji class="g-emoji" alias="arrow_forward">▶️</g-emoji> 《金钱的作用》<br>更真实的合成声音</th>
 84 | <th align="center"><g-emoji class="g-emoji" alias="arrow_forward">▶️</g-emoji> 《生命的意义是什么》</th>
 85 | </tr>
 86 | </thead>
 87 | <tbody>
 88 | <tr>
 89 | <td align="center"><video src="https://github.com/harry0703/MoneyPrinterTurbo/assets/4928832/a84d33d5-27a2-4aba-8fd0-9fb2bd91c6a6"></video></td>
 90 | <td align="center"><video src="https://github.com/harry0703/MoneyPrinterTurbo/assets/4928832/af2f3b0b-002e-49fe-b161-18ba91c055e8"></video></td>
 91 | <td align="center"><video src="https://github.com/harry0703/MoneyPrinterTurbo/assets/4928832/112c9564-d52b-4472-99ad-970b75f66476"></video></td>
 92 | </tr>
 93 | </tbody>
 94 | </table>
 95 | 
 96 | ### 横屏 16:9
 97 | 
 98 | <table>
 99 | <thead>
100 | <tr>
101 | <th align="center"><g-emoji class="g-emoji" alias="arrow_forward">▶️</g-emoji>《生命的意义是什么》</th>
102 | <th align="center"><g-emoji class="g-emoji" alias="arrow_forward">▶️</g-emoji>《为什么要运动》</th>
103 | </tr>
104 | </thead>
105 | <tbody>
106 | <tr>
107 | <td align="center"><video src="https://github.com/harry0703/MoneyPrinterTurbo/assets/4928832/346ebb15-c55f-47a9-a653-114f08bb8073"></video></td>
108 | <td align="center"><video src="https://github.com/harry0703/MoneyPrinterTurbo/assets/4928832/271f2fae-8283-44a0-8aa0-0ed8f9a6fa87"></video></td>
109 | </tr>
110 | </tbody>
111 | </table>
112 | 
113 | ## 配置要求 📦
114 | 
115 | - 建议最低 CPU **4核** 或以上,内存 **4G** 或以上,显卡非必须
116 | - Windows 10 或 MacOS 11.0 以上系统
117 | 
118 | 
119 | ## 快速开始 🚀
120 | 
121 | ### 在 Google Colab 中运行
122 | 免去本地环境配置,点击直接在 Google Colab 中快速体验 MoneyPrinterTurbo
123 | 
124 | [![Open in Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/harry0703/MoneyPrinterTurbo/blob/main/docs/MoneyPrinterTurbo.ipynb)
125 | 
126 | 
127 | ### Windows一键启动包
128 | 
129 | 下载一键启动包,解压直接使用(路径不要有 **中文**、**特殊字符**、**空格**)
130 | 
131 | - 百度网盘(v1.2.6): https://pan.baidu.com/s/1wg0UaIyXpO3SqIpaq790SQ?pwd=sbqx 提取码: sbqx
132 | - Google Drive (v1.2.6): https://drive.google.com/file/d/1HsbzfT7XunkrCrHw5ncUjFX8XX4zAuUh/view?usp=sharing
133 | 
134 | 下载后,建议先**双击执行** `update.bat` 更新到**最新代码**,然后双击 `start.bat` 启动
135 | 
136 | 启动后,会自动打开浏览器(如果打开是空白,建议换成 **Chrome** 或者 **Edge** 打开)
137 | 
138 | ## 安装部署 📥
139 | 
140 | ### 前提条件
141 | 
142 | - 尽量不要使用 **中文路径**,避免出现一些无法预料的问题
143 | - 请确保你的 **网络** 是正常的,VPN需要打开`全局流量`模式
144 | 
145 | #### ① 克隆代码
146 | 
147 | ```shell
148 | git clone https://github.com/harry0703/MoneyPrinterTurbo.git
149 | ```
150 | 
151 | #### ② 修改配置文件(可选,建议启动后也可以在 WebUI 里面配置)
152 | 
153 | - 将 `config.example.toml` 文件复制一份,命名为 `config.toml`
154 | - 按照 `config.toml` 文件中的说明,配置好 `pexels_api_keys` 和 `llm_provider`,并根据 llm_provider 对应的服务商,配置相关的
155 |   API Key
156 | 
157 | ### Docker部署 🐳
158 | 
159 | #### ① 启动Docker
160 | 
161 | 如果未安装 Docker,请先安装 https://www.docker.com/products/docker-desktop/
162 | 
163 | 如果是Windows系统,请参考微软的文档:
164 | 
165 | 1. https://learn.microsoft.com/zh-cn/windows/wsl/install
166 | 2. https://learn.microsoft.com/zh-cn/windows/wsl/tutorials/wsl-containers
167 | 
168 | ```shell
169 | cd MoneyPrinterTurbo
170 | docker-compose up
171 | ```
172 | 
173 | > 注意:最新版的docker安装时会自动以插件的形式安装docker compose,启动命令调整为docker compose up
174 | 
175 | #### ② 访问Web界面
176 | 
177 | 打开浏览器,访问 http://0.0.0.0:8501
178 | 
179 | #### ③ 访问API文档
180 | 
181 | 打开浏览器,访问 http://0.0.0.0:8080/docs 或者 http://0.0.0.0:8080/redoc
182 | 
183 | ### 手动部署 📦
184 | 
185 | > 视频教程
186 | 
187 | - 完整的使用演示:https://v.douyin.com/iFhnwsKY/
188 | - 如何在Windows上部署:https://v.douyin.com/iFyjoW3M
189 | 
190 | #### ① 创建虚拟环境
191 | 
192 | 建议使用 [conda](https://conda.io/projects/conda/en/latest/user-guide/install/index.html) 创建 python 虚拟环境
193 | 
194 | ```shell
195 | git clone https://github.com/harry0703/MoneyPrinterTurbo.git
196 | cd MoneyPrinterTurbo
197 | conda create -n MoneyPrinterTurbo python=3.11
198 | conda activate MoneyPrinterTurbo
199 | pip install -r requirements.txt
200 | ```
201 | 
202 | #### ② 安装好 ImageMagick
203 | 
204 | - Windows:
205 |     - 下载 https://imagemagick.org/script/download.php 选择Windows版本,切记一定要选择 **静态库** 版本,比如
206 |       ImageMagick-7.1.1-32-Q16-x64-**static**.exe
207 |     - 安装下载好的 ImageMagick,**注意不要修改安装路径**
208 |     - 修改 `配置文件 config.toml` 中的 `imagemagick_path` 为你的 **实际安装路径**
209 | 
210 | - MacOS:
211 |   ```shell
212 |   brew install imagemagick
213 |   ````
214 | - Ubuntu
215 |   ```shell
216 |   sudo apt-get install imagemagick
217 |   ```
218 | - CentOS
219 |   ```shell
220 |   sudo yum install ImageMagick
221 |   ```
222 | 
223 | #### ③ 启动Web界面 🌐
224 | 
225 | 注意需要到 MoneyPrinterTurbo 项目 `根目录` 下执行以下命令
226 | 
227 | ###### Windows
228 | 
229 | ```bat
230 | webui.bat
231 | ```
232 | 
233 | ###### MacOS or Linux
234 | 
235 | ```shell
236 | sh webui.sh
237 | ```
238 | 
239 | 启动后,会自动打开浏览器(如果打开是空白,建议换成 **Chrome** 或者 **Edge** 打开)
240 | 
241 | #### ④ 启动API服务 🚀
242 | 
243 | ```shell
244 | python main.py
245 | ```
246 | 
247 | 启动后,可以查看 `API文档` http://127.0.0.1:8080/docs 或者 http://127.0.0.1:8080/redoc 直接在线调试接口,快速体验。
248 | 
249 | ## 语音合成 🗣
250 | 
251 | 所有支持的声音列表,可以查看:[声音列表](./docs/voice-list.txt)
252 | 
253 | 2024-04-16 v1.1.2 新增了9种Azure的语音合成声音,需要配置API KEY,该声音合成的更加真实。
254 | 
255 | ## 字幕生成 📜
256 | 
257 | 当前支持2种字幕生成方式:
258 | 
259 | - **edge**: 生成`速度快`,性能更好,对电脑配置没有要求,但是质量可能不稳定
260 | - **whisper**: 生成`速度慢`,性能较差,对电脑配置有一定要求,但是`质量更可靠`。
261 | 
262 | 可以修改 `config.toml` 配置文件中的 `subtitle_provider` 进行切换
263 | 
264 | 建议使用 `edge` 模式,如果生成的字幕质量不好,再切换到 `whisper` 模式
265 | 
266 | > 注意:
267 | 
268 | 1. whisper 模式下需要到 HuggingFace 下载一个模型文件,大约 3GB 左右,请确保网络通畅
269 | 2. 如果留空,表示不生成字幕。
270 | 
271 | > 由于国内无法访问 HuggingFace,可以使用以下方法下载 `whisper-large-v3` 的模型文件
272 | 
273 | 下载地址:
274 | 
275 | - 百度网盘: https://pan.baidu.com/s/11h3Q6tsDtjQKTjUu3sc5cA?pwd=xjs9
276 | - 夸克网盘:https://pan.quark.cn/s/3ee3d991d64b
277 | 
278 | 模型下载后解压,整个目录放到 `.\MoneyPrinterTurbo\models` 里面,
279 | 最终的文件路径应该是这样: `.\MoneyPrinterTurbo\models\whisper-large-v3`
280 | 
281 | ```
282 | MoneyPrinterTurbo  
283 |   ├─models
284 |   │   └─whisper-large-v3
285 |   │          config.json
286 |   │          model.bin
287 |   │          preprocessor_config.json
288 |   │          tokenizer.json
289 |   │          vocabulary.json
290 | ```
291 | 
292 | ## 背景音乐 🎵
293 | 
294 | 用于视频的背景音乐,位于项目的 `resource/songs` 目录下。
295 | > 当前项目里面放了一些默认的音乐,来自于 YouTube 视频,如有侵权,请删除。
296 | 
297 | ## 字幕字体 🅰
298 | 
299 | 用于视频字幕的渲染,位于项目的 `resource/fonts` 目录下,你也可以放进去自己的字体。
300 | 
301 | ## 常见问题 🤔
302 | 
303 | ### ❓RuntimeError: No ffmpeg exe could be found
304 | 
305 | 通常情况下,ffmpeg 会被自动下载,并且会被自动检测到。
306 | 但是如果你的环境有问题,无法自动下载,可能会遇到如下错误:
307 | 
308 | ```
309 | RuntimeError: No ffmpeg exe could be found.
310 | Install ffmpeg on your system, or set the IMAGEIO_FFMPEG_EXE environment variable.
311 | ```
312 | 
313 | 此时你可以从 https://www.gyan.dev/ffmpeg/builds/ 下载ffmpeg,解压后,设置 `ffmpeg_path` 为你的实际安装路径即可。
314 | 
315 | ```toml
316 | [app]
317 | # 请根据你的实际路径设置,注意 Windows 路径分隔符为 \\
318 | ffmpeg_path = "C:\\Users\\harry\\Downloads\\ffmpeg.exe"
319 | ```
320 | 
321 | ### ❓ImageMagick的安全策略阻止了与临时文件@/tmp/tmpur5hyyto.txt相关的操作
322 | 
323 | 可以在ImageMagick的配置文件policy.xml中找到这些策略。
324 | 这个文件通常位于 /etc/ImageMagick-`X`/ 或 ImageMagick 安装目录的类似位置。
325 | 修改包含`pattern="@"`的条目,将`rights="none"`更改为`rights="read|write"`以允许对文件的读写操作。
326 | 
327 | ### ❓OSError: [Errno 24] Too many open files
328 | 
329 | 这个问题是由于系统打开文件数限制导致的,可以通过修改系统的文件打开数限制来解决。
330 | 
331 | 查看当前限制
332 | 
333 | ```shell
334 | ulimit -n
335 | ```
336 | 
337 | 如果过低,可以调高一些,比如
338 | 
339 | ```shell
340 | ulimit -n 10240
341 | ```
342 | 
343 | ### ❓Whisper 模型下载失败,出现如下错误
344 | 
345 | LocalEntryNotfoundEror: Cannot find an appropriate cached snapshotfolderfor the specified revision on the local disk and
346 | outgoing trafic has been disabled.
347 | To enablerepo look-ups and downloads online, pass 'local files only=False' as input.
348 | 
349 | 或者
350 | 
351 | An error occured while synchronizing the model Systran/faster-whisper-large-v3 from the Hugging Face Hub:
352 | An error happened while trying to locate the files on the Hub and we cannot find the appropriate snapshot folder for the
353 | specified revision on the local disk. Please check your internet connection and try again.
354 | Trying to load the model directly from the local cache, if it exists.
355 | 
356 | 解决方法:[点击查看如何从网盘手动下载模型](#%E5%AD%97%E5%B9%95%E7%94%9F%E6%88%90-)
357 | 
358 | ## 反馈建议 📢
359 | 
360 | - 可以提交 [issue](https://github.com/harry0703/MoneyPrinterTurbo/issues)
361 |   或者 [pull request](https://github.com/harry0703/MoneyPrinterTurbo/pulls)。
362 | 
363 | ## 许可证 📝
364 | 
365 | 点击查看 [`LICENSE`](LICENSE) 文件
366 | 
367 | ## Star History
368 | 
369 | [![Star History Chart](https://api.star-history.com/svg?repos=harry0703/MoneyPrinterTurbo&type=Date)](https://star-history.com/#harry0703/MoneyPrinterTurbo&Date)


--------------------------------------------------------------------------------
/app/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/harry0703/MoneyPrinterTurbo/6cb5f2348714b08fd7cf04a416c13faeab1d356b/app/__init__.py


--------------------------------------------------------------------------------
/app/asgi.py:
--------------------------------------------------------------------------------
 1 | """Application implementation - ASGI."""
 2 | 
 3 | import os
 4 | 
 5 | from fastapi import FastAPI, Request
 6 | from fastapi.exceptions import RequestValidationError
 7 | from fastapi.middleware.cors import CORSMiddleware
 8 | from fastapi.responses import JSONResponse
 9 | from fastapi.staticfiles import StaticFiles
10 | from loguru import logger
11 | 
12 | from app.config import config
13 | from app.models.exception import HttpException
14 | from app.router import root_api_router
15 | from app.utils import utils
16 | 
17 | 
18 | def exception_handler(request: Request, e: HttpException):
19 |     return JSONResponse(
20 |         status_code=e.status_code,
21 |         content=utils.get_response(e.status_code, e.data, e.message),
22 |     )
23 | 
24 | 
25 | def validation_exception_handler(request: Request, e: RequestValidationError):
26 |     return JSONResponse(
27 |         status_code=400,
28 |         content=utils.get_response(
29 |             status=400, data=e.errors(), message="field required"
30 |         ),
31 |     )
32 | 
33 | 
34 | def get_application() -> FastAPI:
35 |     """Initialize FastAPI application.
36 | 
37 |     Returns:
38 |        FastAPI: Application object instance.
39 | 
40 |     """
41 |     instance = FastAPI(
42 |         title=config.project_name,
43 |         description=config.project_description,
44 |         version=config.project_version,
45 |         debug=False,
46 |     )
47 |     instance.include_router(root_api_router)
48 |     instance.add_exception_handler(HttpException, exception_handler)
49 |     instance.add_exception_handler(RequestValidationError, validation_exception_handler)
50 |     return instance
51 | 
52 | 
53 | app = get_application()
54 | 
55 | # Configures the CORS middleware for the FastAPI app
56 | cors_allowed_origins_str = os.getenv("CORS_ALLOWED_ORIGINS", "")
57 | origins = cors_allowed_origins_str.split(",") if cors_allowed_origins_str else ["*"]
58 | app.add_middleware(
59 |     CORSMiddleware,
60 |     allow_origins=origins,
61 |     allow_credentials=True,
62 |     allow_methods=["*"],
63 |     allow_headers=["*"],
64 | )
65 | 
66 | task_dir = utils.task_dir()
67 | app.mount(
68 |     "/tasks", StaticFiles(directory=task_dir, html=True, follow_symlink=True), name=""
69 | )
70 | 
71 | public_dir = utils.public_dir()
72 | app.mount("/", StaticFiles(directory=public_dir, html=True), name="")
73 | 
74 | 
75 | @app.on_event("shutdown")
76 | def shutdown_event():
77 |     logger.info("shutdown event")
78 | 
79 | 
80 | @app.on_event("startup")
81 | def startup_event():
82 |     logger.info("startup event")
83 | 


--------------------------------------------------------------------------------
/app/config/__init__.py:
--------------------------------------------------------------------------------
 1 | import os
 2 | import sys
 3 | 
 4 | from loguru import logger
 5 | 
 6 | from app.config import config
 7 | from app.utils import utils
 8 | 
 9 | 
10 | def __init_logger():
11 |     # _log_file = utils.storage_dir("logs/server.log")
12 |     _lvl = config.log_level
13 |     root_dir = os.path.dirname(
14 |         os.path.dirname(os.path.dirname(os.path.realpath(__file__)))
15 |     )
16 | 
17 |     def format_record(record):
18 |         # 获取日志记录中的文件全路径
19 |         file_path = record["file"].path
20 |         # 将绝对路径转换为相对于项目根目录的路径
21 |         relative_path = os.path.relpath(file_path, root_dir)
22 |         # 更新记录中的文件路径
23 |         record["file"].path = f"./{relative_path}"
24 |         # 返回修改后的格式字符串
25 |         # 您可以根据需要调整这里的格式
26 |         _format = (
27 |             "<green>{time:%Y-%m-%d %H:%M:%S}</> | "
28 |             + "<level>{level}</> | "
29 |             + '"{file.path}:{line}":<blue> {function}</> '
30 |             + "- <level>{message}</>"
31 |             + "\n"
32 |         )
33 |         return _format
34 | 
35 |     logger.remove()
36 | 
37 |     logger.add(
38 |         sys.stdout,
39 |         level=_lvl,
40 |         format=format_record,
41 |         colorize=True,
42 |     )
43 | 
44 |     # logger.add(
45 |     #     _log_file,
46 |     #     level=_lvl,
47 |     #     format=format_record,
48 |     #     rotation="00:00",
49 |     #     retention="3 days",
50 |     #     backtrace=True,
51 |     #     diagnose=True,
52 |     #     enqueue=True,
53 |     # )
54 | 
55 | 
56 | __init_logger()
57 | 


--------------------------------------------------------------------------------
/app/config/config.py:
--------------------------------------------------------------------------------
 1 | import os
 2 | import shutil
 3 | import socket
 4 | 
 5 | import toml
 6 | from loguru import logger
 7 | 
 8 | root_dir = os.path.dirname(os.path.dirname(os.path.dirname(os.path.realpath(__file__))))
 9 | config_file = f"{root_dir}/config.toml"
10 | 
11 | 
12 | def load_config():
13 |     # fix: IsADirectoryError: [Errno 21] Is a directory: '/MoneyPrinterTurbo/config.toml'
14 |     if os.path.isdir(config_file):
15 |         shutil.rmtree(config_file)
16 | 
17 |     if not os.path.isfile(config_file):
18 |         example_file = f"{root_dir}/config.example.toml"
19 |         if os.path.isfile(example_file):
20 |             shutil.copyfile(example_file, config_file)
21 |             logger.info("copy config.example.toml to config.toml")
22 | 
23 |     logger.info(f"load config from file: {config_file}")
24 | 
25 |     try:
26 |         _config_ = toml.load(config_file)
27 |     except Exception as e:
28 |         logger.warning(f"load config failed: {str(e)}, try to load as utf-8-sig")
29 |         with open(config_file, mode="r", encoding="utf-8-sig") as fp:
30 |             _cfg_content = fp.read()
31 |             _config_ = toml.loads(_cfg_content)
32 |     return _config_
33 | 
34 | 
35 | def save_config():
36 |     with open(config_file, "w", encoding="utf-8") as f:
37 |         _cfg["app"] = app
38 |         _cfg["azure"] = azure
39 |         _cfg["siliconflow"] = siliconflow
40 |         _cfg["ui"] = ui
41 |         f.write(toml.dumps(_cfg))
42 | 
43 | 
44 | _cfg = load_config()
45 | app = _cfg.get("app", {})
46 | whisper = _cfg.get("whisper", {})
47 | proxy = _cfg.get("proxy", {})
48 | azure = _cfg.get("azure", {})
49 | siliconflow = _cfg.get("siliconflow", {})
50 | ui = _cfg.get(
51 |     "ui",
52 |     {
53 |         "hide_log": False,
54 |     },
55 | )
56 | 
57 | hostname = socket.gethostname()
58 | 
59 | log_level = _cfg.get("log_level", "DEBUG")
60 | listen_host = _cfg.get("listen_host", "0.0.0.0")
61 | listen_port = _cfg.get("listen_port", 8080)
62 | project_name = _cfg.get("project_name", "MoneyPrinterTurbo")
63 | project_description = _cfg.get(
64 |     "project_description",
65 |     "<a href='https://github.com/harry0703/MoneyPrinterTurbo'>https://github.com/harry0703/MoneyPrinterTurbo</a>",
66 | )
67 | project_version = _cfg.get("project_version", "1.2.6")
68 | reload_debug = False
69 | 
70 | imagemagick_path = app.get("imagemagick_path", "")
71 | if imagemagick_path and os.path.isfile(imagemagick_path):
72 |     os.environ["IMAGEMAGICK_BINARY"] = imagemagick_path
73 | 
74 | ffmpeg_path = app.get("ffmpeg_path", "")
75 | if ffmpeg_path and os.path.isfile(ffmpeg_path):
76 |     os.environ["IMAGEIO_FFMPEG_EXE"] = ffmpeg_path
77 | 
78 | logger.info(f"{project_name} v{project_version}")
79 | 


--------------------------------------------------------------------------------
/app/controllers/base.py:
--------------------------------------------------------------------------------
 1 | from uuid import uuid4
 2 | 
 3 | from fastapi import Request
 4 | 
 5 | from app.config import config
 6 | from app.models.exception import HttpException
 7 | 
 8 | 
 9 | def get_task_id(request: Request):
10 |     task_id = request.headers.get("x-task-id")
11 |     if not task_id:
12 |         task_id = uuid4()
13 |     return str(task_id)
14 | 
15 | 
16 | def get_api_key(request: Request):
17 |     api_key = request.headers.get("x-api-key")
18 |     return api_key
19 | 
20 | 
21 | def verify_token(request: Request):
22 |     token = get_api_key(request)
23 |     if token != config.app.get("api_key", ""):
24 |         request_id = get_task_id(request)
25 |         request_url = request.url
26 |         user_agent = request.headers.get("user-agent")
27 |         raise HttpException(
28 |             task_id=request_id,
29 |             status_code=401,
30 |             message=f"invalid token: {request_url}, {user_agent}",
31 |         )
32 | 


--------------------------------------------------------------------------------
/app/controllers/manager/base_manager.py:
--------------------------------------------------------------------------------
 1 | import threading
 2 | from typing import Any, Callable, Dict
 3 | 
 4 | 
 5 | class TaskManager:
 6 |     def __init__(self, max_concurrent_tasks: int):
 7 |         self.max_concurrent_tasks = max_concurrent_tasks
 8 |         self.current_tasks = 0
 9 |         self.lock = threading.Lock()
10 |         self.queue = self.create_queue()
11 | 
12 |     def create_queue(self):
13 |         raise NotImplementedError()
14 | 
15 |     def add_task(self, func: Callable, *args: Any, **kwargs: Any):
16 |         with self.lock:
17 |             if self.current_tasks < self.max_concurrent_tasks:
18 |                 print(f"add task: {func.__name__}, current_tasks: {self.current_tasks}")
19 |                 self.execute_task(func, *args, **kwargs)
20 |             else:
21 |                 print(
22 |                     f"enqueue task: {func.__name__}, current_tasks: {self.current_tasks}"
23 |                 )
24 |                 self.enqueue({"func": func, "args": args, "kwargs": kwargs})
25 | 
26 |     def execute_task(self, func: Callable, *args: Any, **kwargs: Any):
27 |         thread = threading.Thread(
28 |             target=self.run_task, args=(func, *args), kwargs=kwargs
29 |         )
30 |         thread.start()
31 | 
32 |     def run_task(self, func: Callable, *args: Any, **kwargs: Any):
33 |         try:
34 |             with self.lock:
35 |                 self.current_tasks += 1
36 |             func(*args, **kwargs)  # call the function here, passing *args and **kwargs.
37 |         finally:
38 |             self.task_done()
39 | 
40 |     def check_queue(self):
41 |         with self.lock:
42 |             if (
43 |                 self.current_tasks < self.max_concurrent_tasks
44 |                 and not self.is_queue_empty()
45 |             ):
46 |                 task_info = self.dequeue()
47 |                 func = task_info["func"]
48 |                 args = task_info.get("args", ())
49 |                 kwargs = task_info.get("kwargs", {})
50 |                 self.execute_task(func, *args, **kwargs)
51 | 
52 |     def task_done(self):
53 |         with self.lock:
54 |             self.current_tasks -= 1
55 |         self.check_queue()
56 | 
57 |     def enqueue(self, task: Dict):
58 |         raise NotImplementedError()
59 | 
60 |     def dequeue(self):
61 |         raise NotImplementedError()
62 | 
63 |     def is_queue_empty(self):
64 |         raise NotImplementedError()
65 | 


--------------------------------------------------------------------------------
/app/controllers/manager/memory_manager.py:
--------------------------------------------------------------------------------
 1 | from queue import Queue
 2 | from typing import Dict
 3 | 
 4 | from app.controllers.manager.base_manager import TaskManager
 5 | 
 6 | 
 7 | class InMemoryTaskManager(TaskManager):
 8 |     def create_queue(self):
 9 |         return Queue()
10 | 
11 |     def enqueue(self, task: Dict):
12 |         self.queue.put(task)
13 | 
14 |     def dequeue(self):
15 |         return self.queue.get()
16 | 
17 |     def is_queue_empty(self):
18 |         return self.queue.empty()
19 | 


--------------------------------------------------------------------------------
/app/controllers/manager/redis_manager.py:
--------------------------------------------------------------------------------
 1 | import json
 2 | from typing import Dict
 3 | 
 4 | import redis
 5 | 
 6 | from app.controllers.manager.base_manager import TaskManager
 7 | from app.models.schema import VideoParams
 8 | from app.services import task as tm
 9 | 
10 | FUNC_MAP = {
11 |     "start": tm.start,
12 |     # 'start_test': tm.start_test
13 | }
14 | 
15 | 
16 | class RedisTaskManager(TaskManager):
17 |     def __init__(self, max_concurrent_tasks: int, redis_url: str):
18 |         self.redis_client = redis.Redis.from_url(redis_url)
19 |         super().__init__(max_concurrent_tasks)
20 | 
21 |     def create_queue(self):
22 |         return "task_queue"
23 | 
24 |     def enqueue(self, task: Dict):
25 |         task_with_serializable_params = task.copy()
26 | 
27 |         if "params" in task["kwargs"] and isinstance(
28 |             task["kwargs"]["params"], VideoParams
29 |         ):
30 |             task_with_serializable_params["kwargs"]["params"] = task["kwargs"][
31 |                 "params"
32 |             ].dict()
33 | 
34 |         # 将函数对象转换为其名称
35 |         task_with_serializable_params["func"] = task["func"].__name__
36 |         self.redis_client.rpush(self.queue, json.dumps(task_with_serializable_params))
37 | 
38 |     def dequeue(self):
39 |         task_json = self.redis_client.lpop(self.queue)
40 |         if task_json:
41 |             task_info = json.loads(task_json)
42 |             # 将函数名称转换回函数对象
43 |             task_info["func"] = FUNC_MAP[task_info["func"]]
44 | 
45 |             if "params" in task_info["kwargs"] and isinstance(
46 |                 task_info["kwargs"]["params"], dict
47 |             ):
48 |                 task_info["kwargs"]["params"] = VideoParams(
49 |                     **task_info["kwargs"]["params"]
50 |                 )
51 | 
52 |             return task_info
53 |         return None
54 | 
55 |     def is_queue_empty(self):
56 |         return self.redis_client.llen(self.queue) == 0
57 | 


--------------------------------------------------------------------------------
/app/controllers/ping.py:
--------------------------------------------------------------------------------
 1 | from fastapi import APIRouter, Request
 2 | 
 3 | router = APIRouter()
 4 | 
 5 | 
 6 | @router.get(
 7 |     "/ping",
 8 |     tags=["Health Check"],
 9 |     description="检查服务可用性",
10 |     response_description="pong",
11 | )
12 | def ping(request: Request) -> str:
13 |     return "pong"
14 | 


--------------------------------------------------------------------------------
/app/controllers/v1/base.py:
--------------------------------------------------------------------------------
 1 | from fastapi import APIRouter
 2 | 
 3 | 
 4 | def new_router(dependencies=None):
 5 |     router = APIRouter()
 6 |     router.tags = ["V1"]
 7 |     router.prefix = "/api/v1"
 8 |     # 将认证依赖项应用于所有路由
 9 |     if dependencies:
10 |         router.dependencies = dependencies
11 |     return router
12 | 


--------------------------------------------------------------------------------
/app/controllers/v1/llm.py:
--------------------------------------------------------------------------------
 1 | from fastapi import Request
 2 | 
 3 | from app.controllers.v1.base import new_router
 4 | from app.models.schema import (
 5 |     VideoScriptRequest,
 6 |     VideoScriptResponse,
 7 |     VideoTermsRequest,
 8 |     VideoTermsResponse,
 9 | )
10 | from app.services import llm
11 | from app.utils import utils
12 | 
13 | # authentication dependency
14 | # router = new_router(dependencies=[Depends(base.verify_token)])
15 | router = new_router()
16 | 
17 | 
18 | @router.post(
19 |     "/scripts",
20 |     response_model=VideoScriptResponse,
21 |     summary="Create a script for the video",
22 | )
23 | def generate_video_script(request: Request, body: VideoScriptRequest):
24 |     video_script = llm.generate_script(
25 |         video_subject=body.video_subject,
26 |         language=body.video_language,
27 |         paragraph_number=body.paragraph_number,
28 |     )
29 |     response = {"video_script": video_script}
30 |     return utils.get_response(200, response)
31 | 
32 | 
33 | @router.post(
34 |     "/terms",
35 |     response_model=VideoTermsResponse,
36 |     summary="Generate video terms based on the video script",
37 | )
38 | def generate_video_terms(request: Request, body: VideoTermsRequest):
39 |     video_terms = llm.generate_terms(
40 |         video_subject=body.video_subject,
41 |         video_script=body.video_script,
42 |         amount=body.amount,
43 |     )
44 |     response = {"video_terms": video_terms}
45 |     return utils.get_response(200, response)
46 | 


--------------------------------------------------------------------------------
/app/controllers/v1/video.py:
--------------------------------------------------------------------------------
  1 | import glob
  2 | import os
  3 | import pathlib
  4 | import shutil
  5 | from typing import Union
  6 | 
  7 | from fastapi import BackgroundTasks, Depends, Path, Request, UploadFile
  8 | from fastapi.params import File
  9 | from fastapi.responses import FileResponse, StreamingResponse
 10 | from loguru import logger
 11 | 
 12 | from app.config import config
 13 | from app.controllers import base
 14 | from app.controllers.manager.memory_manager import InMemoryTaskManager
 15 | from app.controllers.manager.redis_manager import RedisTaskManager
 16 | from app.controllers.v1.base import new_router
 17 | from app.models.exception import HttpException
 18 | from app.models.schema import (
 19 |     AudioRequest,
 20 |     BgmRetrieveResponse,
 21 |     BgmUploadResponse,
 22 |     SubtitleRequest,
 23 |     TaskDeletionResponse,
 24 |     TaskQueryRequest,
 25 |     TaskQueryResponse,
 26 |     TaskResponse,
 27 |     TaskVideoRequest,
 28 | )
 29 | from app.services import state as sm
 30 | from app.services import task as tm
 31 | from app.utils import utils
 32 | 
 33 | # 认证依赖项
 34 | # router = new_router(dependencies=[Depends(base.verify_token)])
 35 | router = new_router()
 36 | 
 37 | _enable_redis = config.app.get("enable_redis", False)
 38 | _redis_host = config.app.get("redis_host", "localhost")
 39 | _redis_port = config.app.get("redis_port", 6379)
 40 | _redis_db = config.app.get("redis_db", 0)
 41 | _redis_password = config.app.get("redis_password", None)
 42 | _max_concurrent_tasks = config.app.get("max_concurrent_tasks", 5)
 43 | 
 44 | redis_url = f"redis://:{_redis_password}@{_redis_host}:{_redis_port}/{_redis_db}"
 45 | # 根据配置选择合适的任务管理器
 46 | if _enable_redis:
 47 |     task_manager = RedisTaskManager(
 48 |         max_concurrent_tasks=_max_concurrent_tasks, redis_url=redis_url
 49 |     )
 50 | else:
 51 |     task_manager = InMemoryTaskManager(max_concurrent_tasks=_max_concurrent_tasks)
 52 | 
 53 | 
 54 | @router.post("/videos", response_model=TaskResponse, summary="Generate a short video")
 55 | def create_video(
 56 |     background_tasks: BackgroundTasks, request: Request, body: TaskVideoRequest
 57 | ):
 58 |     return create_task(request, body, stop_at="video")
 59 | 
 60 | 
 61 | @router.post("/subtitle", response_model=TaskResponse, summary="Generate subtitle only")
 62 | def create_subtitle(
 63 |     background_tasks: BackgroundTasks, request: Request, body: SubtitleRequest
 64 | ):
 65 |     return create_task(request, body, stop_at="subtitle")
 66 | 
 67 | 
 68 | @router.post("/audio", response_model=TaskResponse, summary="Generate audio only")
 69 | def create_audio(
 70 |     background_tasks: BackgroundTasks, request: Request, body: AudioRequest
 71 | ):
 72 |     return create_task(request, body, stop_at="audio")
 73 | 
 74 | 
 75 | def create_task(
 76 |     request: Request,
 77 |     body: Union[TaskVideoRequest, SubtitleRequest, AudioRequest],
 78 |     stop_at: str,
 79 | ):
 80 |     task_id = utils.get_uuid()
 81 |     request_id = base.get_task_id(request)
 82 |     try:
 83 |         task = {
 84 |             "task_id": task_id,
 85 |             "request_id": request_id,
 86 |             "params": body.model_dump(),
 87 |         }
 88 |         sm.state.update_task(task_id)
 89 |         task_manager.add_task(tm.start, task_id=task_id, params=body, stop_at=stop_at)
 90 |         logger.success(f"Task created: {utils.to_json(task)}")
 91 |         return utils.get_response(200, task)
 92 |     except ValueError as e:
 93 |         raise HttpException(
 94 |             task_id=task_id, status_code=400, message=f"{request_id}: {str(e)}"
 95 |         )
 96 | 
 97 | from fastapi import Query
 98 | 
 99 | @router.get("/tasks", response_model=TaskQueryResponse, summary="Get all tasks")
100 | def get_all_tasks(request: Request, page: int = Query(1, ge=1), page_size: int = Query(10, ge=1)):
101 |     request_id = base.get_task_id(request)
102 |     tasks, total = sm.state.get_all_tasks(page, page_size)
103 | 
104 |     response = {
105 |         "tasks": tasks,
106 |         "total": total,
107 |         "page": page,
108 |         "page_size": page_size,
109 |     }
110 |     return utils.get_response(200, response)
111 | 
112 | 
113 | 
114 | @router.get(
115 |     "/tasks/{task_id}", response_model=TaskQueryResponse, summary="Query task status"
116 | )
117 | def get_task(
118 |     request: Request,
119 |     task_id: str = Path(..., description="Task ID"),
120 |     query: TaskQueryRequest = Depends(),
121 | ):
122 |     endpoint = config.app.get("endpoint", "")
123 |     if not endpoint:
124 |         endpoint = str(request.base_url)
125 |     endpoint = endpoint.rstrip("/")
126 | 
127 |     request_id = base.get_task_id(request)
128 |     task = sm.state.get_task(task_id)
129 |     if task:
130 |         task_dir = utils.task_dir()
131 | 
132 |         def file_to_uri(file):
133 |             if not file.startswith(endpoint):
134 |                 _uri_path = v.replace(task_dir, "tasks").replace("\\", "/")
135 |                 _uri_path = f"{endpoint}/{_uri_path}"
136 |             else:
137 |                 _uri_path = file
138 |             return _uri_path
139 | 
140 |         if "videos" in task:
141 |             videos = task["videos"]
142 |             urls = []
143 |             for v in videos:
144 |                 urls.append(file_to_uri(v))
145 |             task["videos"] = urls
146 |         if "combined_videos" in task:
147 |             combined_videos = task["combined_videos"]
148 |             urls = []
149 |             for v in combined_videos:
150 |                 urls.append(file_to_uri(v))
151 |             task["combined_videos"] = urls
152 |         return utils.get_response(200, task)
153 | 
154 |     raise HttpException(
155 |         task_id=task_id, status_code=404, message=f"{request_id}: task not found"
156 |     )
157 | 
158 | 
159 | @router.delete(
160 |     "/tasks/{task_id}",
161 |     response_model=TaskDeletionResponse,
162 |     summary="Delete a generated short video task",
163 | )
164 | def delete_video(request: Request, task_id: str = Path(..., description="Task ID")):
165 |     request_id = base.get_task_id(request)
166 |     task = sm.state.get_task(task_id)
167 |     if task:
168 |         tasks_dir = utils.task_dir()
169 |         current_task_dir = os.path.join(tasks_dir, task_id)
170 |         if os.path.exists(current_task_dir):
171 |             shutil.rmtree(current_task_dir)
172 | 
173 |         sm.state.delete_task(task_id)
174 |         logger.success(f"video deleted: {utils.to_json(task)}")
175 |         return utils.get_response(200)
176 | 
177 |     raise HttpException(
178 |         task_id=task_id, status_code=404, message=f"{request_id}: task not found"
179 |     )
180 | 
181 | 
182 | @router.get(
183 |     "/musics", response_model=BgmRetrieveResponse, summary="Retrieve local BGM files"
184 | )
185 | def get_bgm_list(request: Request):
186 |     suffix = "*.mp3"
187 |     song_dir = utils.song_dir()
188 |     files = glob.glob(os.path.join(song_dir, suffix))
189 |     bgm_list = []
190 |     for file in files:
191 |         bgm_list.append(
192 |             {
193 |                 "name": os.path.basename(file),
194 |                 "size": os.path.getsize(file),
195 |                 "file": file,
196 |             }
197 |         )
198 |     response = {"files": bgm_list}
199 |     return utils.get_response(200, response)
200 | 
201 | 
202 | @router.post(
203 |     "/musics",
204 |     response_model=BgmUploadResponse,
205 |     summary="Upload the BGM file to the songs directory",
206 | )
207 | def upload_bgm_file(request: Request, file: UploadFile = File(...)):
208 |     request_id = base.get_task_id(request)
209 |     # check file ext
210 |     if file.filename.endswith("mp3"):
211 |         song_dir = utils.song_dir()
212 |         save_path = os.path.join(song_dir, file.filename)
213 |         # save file
214 |         with open(save_path, "wb+") as buffer:
215 |             # If the file already exists, it will be overwritten
216 |             file.file.seek(0)
217 |             buffer.write(file.file.read())
218 |         response = {"file": save_path}
219 |         return utils.get_response(200, response)
220 | 
221 |     raise HttpException(
222 |         "", status_code=400, message=f"{request_id}: Only *.mp3 files can be uploaded"
223 |     )
224 | 
225 | 
226 | @router.get("/stream/{file_path:path}")
227 | async def stream_video(request: Request, file_path: str):
228 |     tasks_dir = utils.task_dir()
229 |     video_path = os.path.join(tasks_dir, file_path)
230 |     range_header = request.headers.get("Range")
231 |     video_size = os.path.getsize(video_path)
232 |     start, end = 0, video_size - 1
233 | 
234 |     length = video_size
235 |     if range_header:
236 |         range_ = range_header.split("bytes=")[1]
237 |         start, end = [int(part) if part else None for part in range_.split("-")]
238 |         if start is None:
239 |             start = video_size - end
240 |             end = video_size - 1
241 |         if end is None:
242 |             end = video_size - 1
243 |         length = end - start + 1
244 | 
245 |     def file_iterator(file_path, offset=0, bytes_to_read=None):
246 |         with open(file_path, "rb") as f:
247 |             f.seek(offset, os.SEEK_SET)
248 |             remaining = bytes_to_read or video_size
249 |             while remaining > 0:
250 |                 bytes_to_read = min(4096, remaining)
251 |                 data = f.read(bytes_to_read)
252 |                 if not data:
253 |                     break
254 |                 remaining -= len(data)
255 |                 yield data
256 | 
257 |     response = StreamingResponse(
258 |         file_iterator(video_path, start, length), media_type="video/mp4"
259 |     )
260 |     response.headers["Content-Range"] = f"bytes {start}-{end}/{video_size}"
261 |     response.headers["Accept-Ranges"] = "bytes"
262 |     response.headers["Content-Length"] = str(length)
263 |     response.status_code = 206  # Partial Content
264 | 
265 |     return response
266 | 
267 | 
268 | @router.get("/download/{file_path:path}")
269 | async def download_video(_: Request, file_path: str):
270 |     """
271 |     download video
272 |     :param _: Request request
273 |     :param file_path: video file path, eg: /cd1727ed-3473-42a2-a7da-4faafafec72b/final-1.mp4
274 |     :return: video file
275 |     """
276 |     tasks_dir = utils.task_dir()
277 |     video_path = os.path.join(tasks_dir, file_path)
278 |     file_path = pathlib.Path(video_path)
279 |     filename = file_path.stem
280 |     extension = file_path.suffix
281 |     headers = {"Content-Disposition": f"attachment; filename={filename}{extension}"}
282 |     return FileResponse(
283 |         path=video_path,
284 |         headers=headers,
285 |         filename=f"{filename}{extension}",
286 |         media_type=f"video/{extension[1:]}",
287 |     )
288 | 


--------------------------------------------------------------------------------
/app/models/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/harry0703/MoneyPrinterTurbo/6cb5f2348714b08fd7cf04a416c13faeab1d356b/app/models/__init__.py


--------------------------------------------------------------------------------
/app/models/const.py:
--------------------------------------------------------------------------------
 1 | PUNCTUATIONS = [
 2 |     "?",
 3 |     ",",
 4 |     ".",
 5 |     "、",
 6 |     ";",
 7 |     ":",
 8 |     "!",
 9 |     "…",
10 |     "?",
11 |     ",",
12 |     "。",
13 |     "、",
14 |     ";",
15 |     ":",
16 |     "!",
17 |     "...",
18 | ]
19 | 
20 | TASK_STATE_FAILED = -1
21 | TASK_STATE_COMPLETE = 1
22 | TASK_STATE_PROCESSING = 4
23 | 
24 | FILE_TYPE_VIDEOS = ["mp4", "mov", "mkv", "webm"]
25 | FILE_TYPE_IMAGES = ["jpg", "jpeg", "png", "bmp"]
26 | 


--------------------------------------------------------------------------------
/app/models/exception.py:
--------------------------------------------------------------------------------
 1 | import traceback
 2 | from typing import Any
 3 | 
 4 | from loguru import logger
 5 | 
 6 | 
 7 | class HttpException(Exception):
 8 |     def __init__(
 9 |         self, task_id: str, status_code: int, message: str = "", data: Any = None
10 |     ):
11 |         self.message = message
12 |         self.status_code = status_code
13 |         self.data = data
14 |         # Retrieve the exception stack trace information.
15 |         tb_str = traceback.format_exc().strip()
16 |         if not tb_str or tb_str == "NoneType: None":
17 |             msg = f"HttpException: {status_code}, {task_id}, {message}"
18 |         else:
19 |             msg = f"HttpException: {status_code}, {task_id}, {message}\n{tb_str}"
20 | 
21 |         if status_code == 400:
22 |             logger.warning(msg)
23 |         else:
24 |             logger.error(msg)
25 | 
26 | 
27 | class FileNotFoundException(Exception):
28 |     pass
29 | 


--------------------------------------------------------------------------------
/app/models/schema.py:
--------------------------------------------------------------------------------
  1 | import warnings
  2 | from enum import Enum
  3 | from typing import Any, List, Optional, Union
  4 | 
  5 | import pydantic
  6 | from pydantic import BaseModel
  7 | 
  8 | # 忽略 Pydantic 的特定警告
  9 | warnings.filterwarnings(
 10 |     "ignore",
 11 |     category=UserWarning,
 12 |     message="Field name.*shadows an attribute in parent.*",
 13 | )
 14 | 
 15 | 
 16 | class VideoConcatMode(str, Enum):
 17 |     random = "random"
 18 |     sequential = "sequential"
 19 | 
 20 | 
 21 | class VideoTransitionMode(str, Enum):
 22 |     none = None
 23 |     shuffle = "Shuffle"
 24 |     fade_in = "FadeIn"
 25 |     fade_out = "FadeOut"
 26 |     slide_in = "SlideIn"
 27 |     slide_out = "SlideOut"
 28 | 
 29 | 
 30 | class VideoAspect(str, Enum):
 31 |     landscape = "16:9"
 32 |     portrait = "9:16"
 33 |     square = "1:1"
 34 | 
 35 |     def to_resolution(self):
 36 |         if self == VideoAspect.landscape.value:
 37 |             return 1920, 1080
 38 |         elif self == VideoAspect.portrait.value:
 39 |             return 1080, 1920
 40 |         elif self == VideoAspect.square.value:
 41 |             return 1080, 1080
 42 |         return 1080, 1920
 43 | 
 44 | 
 45 | class _Config:
 46 |     arbitrary_types_allowed = True
 47 | 
 48 | 
 49 | @pydantic.dataclasses.dataclass(config=_Config)
 50 | class MaterialInfo:
 51 |     provider: str = "pexels"
 52 |     url: str = ""
 53 |     duration: int = 0
 54 | 
 55 | 
 56 | class VideoParams(BaseModel):
 57 |     """
 58 |     {
 59 |       "video_subject": "",
 60 |       "video_aspect": "横屏 16:9(西瓜视频)",
 61 |       "voice_name": "女生-晓晓",
 62 |       "bgm_name": "random",
 63 |       "font_name": "STHeitiMedium 黑体-中",
 64 |       "text_color": "#FFFFFF",
 65 |       "font_size": 60,
 66 |       "stroke_color": "#000000",
 67 |       "stroke_width": 1.5
 68 |     }
 69 |     """
 70 | 
 71 |     video_subject: str
 72 |     video_script: str = ""  # Script used to generate the video
 73 |     video_terms: Optional[str | list] = None  # Keywords used to generate the video
 74 |     video_aspect: Optional[VideoAspect] = VideoAspect.portrait.value
 75 |     video_concat_mode: Optional[VideoConcatMode] = VideoConcatMode.random.value
 76 |     video_transition_mode: Optional[VideoTransitionMode] = None
 77 |     video_clip_duration: Optional[int] = 5
 78 |     video_count: Optional[int] = 1
 79 | 
 80 |     video_source: Optional[str] = "pexels"
 81 |     video_materials: Optional[List[MaterialInfo]] = (
 82 |         None  # Materials used to generate the video
 83 |     )
 84 | 
 85 |     video_language: Optional[str] = ""  # auto detect
 86 | 
 87 |     voice_name: Optional[str] = ""
 88 |     voice_volume: Optional[float] = 1.0
 89 |     voice_rate: Optional[float] = 1.0
 90 |     bgm_type: Optional[str] = "random"
 91 |     bgm_file: Optional[str] = ""
 92 |     bgm_volume: Optional[float] = 0.2
 93 | 
 94 |     subtitle_enabled: Optional[bool] = True
 95 |     subtitle_position: Optional[str] = "bottom"  # top, bottom, center
 96 |     custom_position: float = 70.0
 97 |     font_name: Optional[str] = "STHeitiMedium.ttc"
 98 |     text_fore_color: Optional[str] = "#FFFFFF"
 99 |     text_background_color: Union[bool, str] = True
100 | 
101 |     font_size: int = 60
102 |     stroke_color: Optional[str] = "#000000"
103 |     stroke_width: float = 1.5
104 |     n_threads: Optional[int] = 2
105 |     paragraph_number: Optional[int] = 1
106 | 
107 | 
108 | class SubtitleRequest(BaseModel):
109 |     video_script: str
110 |     video_language: Optional[str] = ""
111 |     voice_name: Optional[str] = "zh-CN-XiaoxiaoNeural-Female"
112 |     voice_volume: Optional[float] = 1.0
113 |     voice_rate: Optional[float] = 1.2
114 |     bgm_type: Optional[str] = "random"
115 |     bgm_file: Optional[str] = ""
116 |     bgm_volume: Optional[float] = 0.2
117 |     subtitle_position: Optional[str] = "bottom"
118 |     font_name: Optional[str] = "STHeitiMedium.ttc"
119 |     text_fore_color: Optional[str] = "#FFFFFF"
120 |     text_background_color: Union[bool, str] = True
121 |     font_size: int = 60
122 |     stroke_color: Optional[str] = "#000000"
123 |     stroke_width: float = 1.5
124 |     video_source: Optional[str] = "local"
125 |     subtitle_enabled: Optional[str] = "true"
126 | 
127 | 
128 | class AudioRequest(BaseModel):
129 |     video_script: str
130 |     video_language: Optional[str] = ""
131 |     voice_name: Optional[str] = "zh-CN-XiaoxiaoNeural-Female"
132 |     voice_volume: Optional[float] = 1.0
133 |     voice_rate: Optional[float] = 1.2
134 |     bgm_type: Optional[str] = "random"
135 |     bgm_file: Optional[str] = ""
136 |     bgm_volume: Optional[float] = 0.2
137 |     video_source: Optional[str] = "local"
138 | 
139 | 
140 | class VideoScriptParams:
141 |     """
142 |     {
143 |       "video_subject": "春天的花海",
144 |       "video_language": "",
145 |       "paragraph_number": 1
146 |     }
147 |     """
148 | 
149 |     video_subject: Optional[str] = "春天的花海"
150 |     video_language: Optional[str] = ""
151 |     paragraph_number: Optional[int] = 1
152 | 
153 | 
154 | class VideoTermsParams:
155 |     """
156 |     {
157 |       "video_subject": "",
158 |       "video_script": "",
159 |       "amount": 5
160 |     }
161 |     """
162 | 
163 |     video_subject: Optional[str] = "春天的花海"
164 |     video_script: Optional[str] = (
165 |         "春天的花海,如诗如画般展现在眼前。万物复苏的季节里,大地披上了一袭绚丽多彩的盛装。金黄的迎春、粉嫩的樱花、洁白的梨花、艳丽的郁金香……"
166 |     )
167 |     amount: Optional[int] = 5
168 | 
169 | 
170 | class BaseResponse(BaseModel):
171 |     status: int = 200
172 |     message: Optional[str] = "success"
173 |     data: Any = None
174 | 
175 | 
176 | class TaskVideoRequest(VideoParams, BaseModel):
177 |     pass
178 | 
179 | 
180 | class TaskQueryRequest(BaseModel):
181 |     pass
182 | 
183 | 
184 | class VideoScriptRequest(VideoScriptParams, BaseModel):
185 |     pass
186 | 
187 | 
188 | class VideoTermsRequest(VideoTermsParams, BaseModel):
189 |     pass
190 | 
191 | 
192 | ######################################################################################################
193 | ######################################################################################################
194 | ######################################################################################################
195 | ######################################################################################################
196 | class TaskResponse(BaseResponse):
197 |     class TaskResponseData(BaseModel):
198 |         task_id: str
199 | 
200 |     data: TaskResponseData
201 | 
202 |     class Config:
203 |         json_schema_extra = {
204 |             "example": {
205 |                 "status": 200,
206 |                 "message": "success",
207 |                 "data": {"task_id": "6c85c8cc-a77a-42b9-bc30-947815aa0558"},
208 |             },
209 |         }
210 | 
211 | 
212 | class TaskQueryResponse(BaseResponse):
213 |     class Config:
214 |         json_schema_extra = {
215 |             "example": {
216 |                 "status": 200,
217 |                 "message": "success",
218 |                 "data": {
219 |                     "state": 1,
220 |                     "progress": 100,
221 |                     "videos": [
222 |                         "http://127.0.0.1:8080/tasks/6c85c8cc-a77a-42b9-bc30-947815aa0558/final-1.mp4"
223 |                     ],
224 |                     "combined_videos": [
225 |                         "http://127.0.0.1:8080/tasks/6c85c8cc-a77a-42b9-bc30-947815aa0558/combined-1.mp4"
226 |                     ],
227 |                 },
228 |             },
229 |         }
230 | 
231 | 
232 | class TaskDeletionResponse(BaseResponse):
233 |     class Config:
234 |         json_schema_extra = {
235 |             "example": {
236 |                 "status": 200,
237 |                 "message": "success",
238 |                 "data": {
239 |                     "state": 1,
240 |                     "progress": 100,
241 |                     "videos": [
242 |                         "http://127.0.0.1:8080/tasks/6c85c8cc-a77a-42b9-bc30-947815aa0558/final-1.mp4"
243 |                     ],
244 |                     "combined_videos": [
245 |                         "http://127.0.0.1:8080/tasks/6c85c8cc-a77a-42b9-bc30-947815aa0558/combined-1.mp4"
246 |                     ],
247 |                 },
248 |             },
249 |         }
250 | 
251 | 
252 | class VideoScriptResponse(BaseResponse):
253 |     class Config:
254 |         json_schema_extra = {
255 |             "example": {
256 |                 "status": 200,
257 |                 "message": "success",
258 |                 "data": {
259 |                     "video_script": "春天的花海,是大自然的一幅美丽画卷。在这个季节里,大地复苏,万物生长,花朵争相绽放,形成了一片五彩斑斓的花海..."
260 |                 },
261 |             },
262 |         }
263 | 
264 | 
265 | class VideoTermsResponse(BaseResponse):
266 |     class Config:
267 |         json_schema_extra = {
268 |             "example": {
269 |                 "status": 200,
270 |                 "message": "success",
271 |                 "data": {"video_terms": ["sky", "tree"]},
272 |             },
273 |         }
274 | 
275 | 
276 | class BgmRetrieveResponse(BaseResponse):
277 |     class Config:
278 |         json_schema_extra = {
279 |             "example": {
280 |                 "status": 200,
281 |                 "message": "success",
282 |                 "data": {
283 |                     "files": [
284 |                         {
285 |                             "name": "output013.mp3",
286 |                             "size": 1891269,
287 |                             "file": "/MoneyPrinterTurbo/resource/songs/output013.mp3",
288 |                         }
289 |                     ]
290 |                 },
291 |             },
292 |         }
293 | 
294 | 
295 | class BgmUploadResponse(BaseResponse):
296 |     class Config:
297 |         json_schema_extra = {
298 |             "example": {
299 |                 "status": 200,
300 |                 "message": "success",
301 |                 "data": {"file": "/MoneyPrinterTurbo/resource/songs/example.mp3"},
302 |             },
303 |         }
304 | 


--------------------------------------------------------------------------------
/app/router.py:
--------------------------------------------------------------------------------
 1 | """Application configuration - root APIRouter.
 2 | 
 3 | Defines all FastAPI application endpoints.
 4 | 
 5 | Resources:
 6 |     1. https://fastapi.tiangolo.com/tutorial/bigger-applications
 7 | 
 8 | """
 9 | 
10 | from fastapi import APIRouter
11 | 
12 | from app.controllers.v1 import llm, video
13 | 
14 | root_api_router = APIRouter()
15 | # v1
16 | root_api_router.include_router(video.router)
17 | root_api_router.include_router(llm.router)
18 | 


--------------------------------------------------------------------------------
/app/services/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/harry0703/MoneyPrinterTurbo/6cb5f2348714b08fd7cf04a416c13faeab1d356b/app/services/__init__.py


--------------------------------------------------------------------------------
/app/services/material.py:
--------------------------------------------------------------------------------
  1 | import os
  2 | import random
  3 | from typing import List
  4 | from urllib.parse import urlencode
  5 | 
  6 | import requests
  7 | from loguru import logger
  8 | from moviepy.video.io.VideoFileClip import VideoFileClip
  9 | 
 10 | from app.config import config
 11 | from app.models.schema import MaterialInfo, VideoAspect, VideoConcatMode
 12 | from app.utils import utils
 13 | 
 14 | requested_count = 0
 15 | 
 16 | 
 17 | def get_api_key(cfg_key: str):
 18 |     api_keys = config.app.get(cfg_key)
 19 |     if not api_keys:
 20 |         raise ValueError(
 21 |             f"\n\n##### {cfg_key} is not set #####\n\nPlease set it in the config.toml file: {config.config_file}\n\n"
 22 |             f"{utils.to_json(config.app)}"
 23 |         )
 24 | 
 25 |     # if only one key is provided, return it
 26 |     if isinstance(api_keys, str):
 27 |         return api_keys
 28 | 
 29 |     global requested_count
 30 |     requested_count += 1
 31 |     return api_keys[requested_count % len(api_keys)]
 32 | 
 33 | 
 34 | def search_videos_pexels(
 35 |     search_term: str,
 36 |     minimum_duration: int,
 37 |     video_aspect: VideoAspect = VideoAspect.portrait,
 38 | ) -> List[MaterialInfo]:
 39 |     aspect = VideoAspect(video_aspect)
 40 |     video_orientation = aspect.name
 41 |     video_width, video_height = aspect.to_resolution()
 42 |     api_key = get_api_key("pexels_api_keys")
 43 |     headers = {
 44 |         "Authorization": api_key,
 45 |         "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/115.0.0.0 Safari/537.36",
 46 |     }
 47 |     # Build URL
 48 |     params = {"query": search_term, "per_page": 20, "orientation": video_orientation}
 49 |     query_url = f"https://api.pexels.com/videos/search?{urlencode(params)}"
 50 |     logger.info(f"searching videos: {query_url}, with proxies: {config.proxy}")
 51 | 
 52 |     try:
 53 |         r = requests.get(
 54 |             query_url,
 55 |             headers=headers,
 56 |             proxies=config.proxy,
 57 |             verify=False,
 58 |             timeout=(30, 60),
 59 |         )
 60 |         response = r.json()
 61 |         video_items = []
 62 |         if "videos" not in response:
 63 |             logger.error(f"search videos failed: {response}")
 64 |             return video_items
 65 |         videos = response["videos"]
 66 |         # loop through each video in the result
 67 |         for v in videos:
 68 |             duration = v["duration"]
 69 |             # check if video has desired minimum duration
 70 |             if duration < minimum_duration:
 71 |                 continue
 72 |             video_files = v["video_files"]
 73 |             # loop through each url to determine the best quality
 74 |             for video in video_files:
 75 |                 w = int(video["width"])
 76 |                 h = int(video["height"])
 77 |                 if w == video_width and h == video_height:
 78 |                     item = MaterialInfo()
 79 |                     item.provider = "pexels"
 80 |                     item.url = video["link"]
 81 |                     item.duration = duration
 82 |                     video_items.append(item)
 83 |                     break
 84 |         return video_items
 85 |     except Exception as e:
 86 |         logger.error(f"search videos failed: {str(e)}")
 87 | 
 88 |     return []
 89 | 
 90 | 
 91 | def search_videos_pixabay(
 92 |     search_term: str,
 93 |     minimum_duration: int,
 94 |     video_aspect: VideoAspect = VideoAspect.portrait,
 95 | ) -> List[MaterialInfo]:
 96 |     aspect = VideoAspect(video_aspect)
 97 | 
 98 |     video_width, video_height = aspect.to_resolution()
 99 | 
100 |     api_key = get_api_key("pixabay_api_keys")
101 |     # Build URL
102 |     params = {
103 |         "q": search_term,
104 |         "video_type": "all",  # Accepted values: "all", "film", "animation"
105 |         "per_page": 50,
106 |         "key": api_key,
107 |     }
108 |     query_url = f"https://pixabay.com/api/videos/?{urlencode(params)}"
109 |     logger.info(f"searching videos: {query_url}, with proxies: {config.proxy}")
110 | 
111 |     try:
112 |         r = requests.get(
113 |             query_url, proxies=config.proxy, verify=False, timeout=(30, 60)
114 |         )
115 |         response = r.json()
116 |         video_items = []
117 |         if "hits" not in response:
118 |             logger.error(f"search videos failed: {response}")
119 |             return video_items
120 |         videos = response["hits"]
121 |         # loop through each video in the result
122 |         for v in videos:
123 |             duration = v["duration"]
124 |             # check if video has desired minimum duration
125 |             if duration < minimum_duration:
126 |                 continue
127 |             video_files = v["videos"]
128 |             # loop through each url to determine the best quality
129 |             for video_type in video_files:
130 |                 video = video_files[video_type]
131 |                 w = int(video["width"])
132 |                 # h = int(video["height"])
133 |                 if w >= video_width:
134 |                     item = MaterialInfo()
135 |                     item.provider = "pixabay"
136 |                     item.url = video["url"]
137 |                     item.duration = duration
138 |                     video_items.append(item)
139 |                     break
140 |         return video_items
141 |     except Exception as e:
142 |         logger.error(f"search videos failed: {str(e)}")
143 | 
144 |     return []
145 | 
146 | 
147 | def save_video(video_url: str, save_dir: str = "") -> str:
148 |     if not save_dir:
149 |         save_dir = utils.storage_dir("cache_videos")
150 | 
151 |     if not os.path.exists(save_dir):
152 |         os.makedirs(save_dir)
153 | 
154 |     url_without_query = video_url.split("?")[0]
155 |     url_hash = utils.md5(url_without_query)
156 |     video_id = f"vid-{url_hash}"
157 |     video_path = f"{save_dir}/{video_id}.mp4"
158 | 
159 |     # if video already exists, return the path
160 |     if os.path.exists(video_path) and os.path.getsize(video_path) > 0:
161 |         logger.info(f"video already exists: {video_path}")
162 |         return video_path
163 | 
164 |     headers = {
165 |         "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/115.0.0.0 Safari/537.36"
166 |     }
167 | 
168 |     # if video does not exist, download it
169 |     with open(video_path, "wb") as f:
170 |         f.write(
171 |             requests.get(
172 |                 video_url,
173 |                 headers=headers,
174 |                 proxies=config.proxy,
175 |                 verify=False,
176 |                 timeout=(60, 240),
177 |             ).content
178 |         )
179 | 
180 |     if os.path.exists(video_path) and os.path.getsize(video_path) > 0:
181 |         try:
182 |             clip = VideoFileClip(video_path)
183 |             duration = clip.duration
184 |             fps = clip.fps
185 |             clip.close()
186 |             if duration > 0 and fps > 0:
187 |                 return video_path
188 |         except Exception as e:
189 |             try:
190 |                 os.remove(video_path)
191 |             except Exception:
192 |                 pass
193 |             logger.warning(f"invalid video file: {video_path} => {str(e)}")
194 |     return ""
195 | 
196 | 
197 | def download_videos(
198 |     task_id: str,
199 |     search_terms: List[str],
200 |     source: str = "pexels",
201 |     video_aspect: VideoAspect = VideoAspect.portrait,
202 |     video_contact_mode: VideoConcatMode = VideoConcatMode.random,
203 |     audio_duration: float = 0.0,
204 |     max_clip_duration: int = 5,
205 | ) -> List[str]:
206 |     valid_video_items = []
207 |     valid_video_urls = []
208 |     found_duration = 0.0
209 |     search_videos = search_videos_pexels
210 |     if source == "pixabay":
211 |         search_videos = search_videos_pixabay
212 | 
213 |     for search_term in search_terms:
214 |         video_items = search_videos(
215 |             search_term=search_term,
216 |             minimum_duration=max_clip_duration,
217 |             video_aspect=video_aspect,
218 |         )
219 |         logger.info(f"found {len(video_items)} videos for '{search_term}'")
220 | 
221 |         for item in video_items:
222 |             if item.url not in valid_video_urls:
223 |                 valid_video_items.append(item)
224 |                 valid_video_urls.append(item.url)
225 |                 found_duration += item.duration
226 | 
227 |     logger.info(
228 |         f"found total videos: {len(valid_video_items)}, required duration: {audio_duration} seconds, found duration: {found_duration} seconds"
229 |     )
230 |     video_paths = []
231 | 
232 |     material_directory = config.app.get("material_directory", "").strip()
233 |     if material_directory == "task":
234 |         material_directory = utils.task_dir(task_id)
235 |     elif material_directory and not os.path.isdir(material_directory):
236 |         material_directory = ""
237 | 
238 |     if video_contact_mode.value == VideoConcatMode.random.value:
239 |         random.shuffle(valid_video_items)
240 | 
241 |     total_duration = 0.0
242 |     for item in valid_video_items:
243 |         try:
244 |             logger.info(f"downloading video: {item.url}")
245 |             saved_video_path = save_video(
246 |                 video_url=item.url, save_dir=material_directory
247 |             )
248 |             if saved_video_path:
249 |                 logger.info(f"video saved: {saved_video_path}")
250 |                 video_paths.append(saved_video_path)
251 |                 seconds = min(max_clip_duration, item.duration)
252 |                 total_duration += seconds
253 |                 if total_duration > audio_duration:
254 |                     logger.info(
255 |                         f"total duration of downloaded videos: {total_duration} seconds, skip downloading more"
256 |                     )
257 |                     break
258 |         except Exception as e:
259 |             logger.error(f"failed to download video: {utils.to_json(item)} => {str(e)}")
260 |     logger.success(f"downloaded {len(video_paths)} videos")
261 |     return video_paths
262 | 
263 | 
264 | if __name__ == "__main__":
265 |     download_videos(
266 |         "test123", ["Money Exchange Medium"], audio_duration=100, source="pixabay"
267 |     )
268 | 


--------------------------------------------------------------------------------
/app/services/state.py:
--------------------------------------------------------------------------------
  1 | import ast
  2 | from abc import ABC, abstractmethod
  3 | 
  4 | from app.config import config
  5 | from app.models import const
  6 | 
  7 | 
  8 | # Base class for state management
  9 | class BaseState(ABC):
 10 |     @abstractmethod
 11 |     def update_task(self, task_id: str, state: int, progress: int = 0, **kwargs):
 12 |         pass
 13 | 
 14 |     @abstractmethod
 15 |     def get_task(self, task_id: str):
 16 |         pass
 17 | 
 18 |     @abstractmethod
 19 |     def get_all_tasks(self, page: int, page_size: int):
 20 |         pass
 21 | 
 22 | 
 23 | # Memory state management
 24 | class MemoryState(BaseState):
 25 |     def __init__(self):
 26 |         self._tasks = {}
 27 | 
 28 |     def get_all_tasks(self, page: int, page_size: int):
 29 |         start = (page - 1) * page_size
 30 |         end = start + page_size
 31 |         tasks = list(self._tasks.values())
 32 |         total = len(tasks)
 33 |         return tasks[start:end], total
 34 | 
 35 |     def update_task(
 36 |         self,
 37 |         task_id: str,
 38 |         state: int = const.TASK_STATE_PROCESSING,
 39 |         progress: int = 0,
 40 |         **kwargs,
 41 |     ):
 42 |         progress = int(progress)
 43 |         if progress > 100:
 44 |             progress = 100
 45 | 
 46 |         self._tasks[task_id] = {
 47 |             "task_id": task_id,
 48 |             "state": state,
 49 |             "progress": progress,
 50 |             **kwargs,
 51 |         }
 52 | 
 53 |     def get_task(self, task_id: str):
 54 |         return self._tasks.get(task_id, None)
 55 | 
 56 |     def delete_task(self, task_id: str):
 57 |         if task_id in self._tasks:
 58 |             del self._tasks[task_id]
 59 | 
 60 | 
 61 | # Redis state management
 62 | class RedisState(BaseState):
 63 |     def __init__(self, host="localhost", port=6379, db=0, password=None):
 64 |         import redis
 65 | 
 66 |         self._redis = redis.StrictRedis(host=host, port=port, db=db, password=password)
 67 | 
 68 |     def get_all_tasks(self, page: int, page_size: int):
 69 |         start = (page - 1) * page_size
 70 |         end = start + page_size
 71 |         tasks = []
 72 |         cursor = 0
 73 |         total = 0
 74 |         while True:
 75 |             cursor, keys = self._redis.scan(cursor, count=page_size)
 76 |             total += len(keys)
 77 |             if total > start:
 78 |                 for key in keys[max(0, start - total):end - total]:
 79 |                     task_data = self._redis.hgetall(key)
 80 |                     task = {
 81 |                         k.decode("utf-8"): self._convert_to_original_type(v) for k, v in task_data.items()
 82 |                     }
 83 |                     tasks.append(task)
 84 |                     if len(tasks) >= page_size:
 85 |                         break
 86 |             if cursor == 0 or len(tasks) >= page_size:
 87 |                 break
 88 |         return tasks, total
 89 | 
 90 |     def update_task(
 91 |         self,
 92 |         task_id: str,
 93 |         state: int = const.TASK_STATE_PROCESSING,
 94 |         progress: int = 0,
 95 |         **kwargs,
 96 |     ):
 97 |         progress = int(progress)
 98 |         if progress > 100:
 99 |             progress = 100
100 | 
101 |         fields = {
102 |             "task_id": task_id,
103 |             "state": state,
104 |             "progress": progress,
105 |             **kwargs,
106 |         }
107 | 
108 |         for field, value in fields.items():
109 |             self._redis.hset(task_id, field, str(value))
110 | 
111 |     def get_task(self, task_id: str):
112 |         task_data = self._redis.hgetall(task_id)
113 |         if not task_data:
114 |             return None
115 | 
116 |         task = {
117 |             key.decode("utf-8"): self._convert_to_original_type(value)
118 |             for key, value in task_data.items()
119 |         }
120 |         return task
121 | 
122 |     def delete_task(self, task_id: str):
123 |         self._redis.delete(task_id)
124 | 
125 |     @staticmethod
126 |     def _convert_to_original_type(value):
127 |         """
128 |         Convert the value from byte string to its original data type.
129 |         You can extend this method to handle other data types as needed.
130 |         """
131 |         value_str = value.decode("utf-8")
132 | 
133 |         try:
134 |             # try to convert byte string array to list
135 |             return ast.literal_eval(value_str)
136 |         except (ValueError, SyntaxError):
137 |             pass
138 | 
139 |         if value_str.isdigit():
140 |             return int(value_str)
141 |         # Add more conversions here if needed
142 |         return value_str
143 | 
144 | 
145 | # Global state
146 | _enable_redis = config.app.get("enable_redis", False)
147 | _redis_host = config.app.get("redis_host", "localhost")
148 | _redis_port = config.app.get("redis_port", 6379)
149 | _redis_db = config.app.get("redis_db", 0)
150 | _redis_password = config.app.get("redis_password", None)
151 | 
152 | state = (
153 |     RedisState(
154 |         host=_redis_host, port=_redis_port, db=_redis_db, password=_redis_password
155 |     )
156 |     if _enable_redis
157 |     else MemoryState()
158 | )
159 | 


--------------------------------------------------------------------------------
/app/services/subtitle.py:
--------------------------------------------------------------------------------
  1 | import json
  2 | import os.path
  3 | import re
  4 | from timeit import default_timer as timer
  5 | 
  6 | from faster_whisper import WhisperModel
  7 | from loguru import logger
  8 | 
  9 | from app.config import config
 10 | from app.utils import utils
 11 | 
 12 | model_size = config.whisper.get("model_size", "large-v3")
 13 | device = config.whisper.get("device", "cpu")
 14 | compute_type = config.whisper.get("compute_type", "int8")
 15 | model = None
 16 | 
 17 | 
 18 | def create(audio_file, subtitle_file: str = ""):
 19 |     global model
 20 |     if not model:
 21 |         model_path = f"{utils.root_dir()}/models/whisper-{model_size}"
 22 |         model_bin_file = f"{model_path}/model.bin"
 23 |         if not os.path.isdir(model_path) or not os.path.isfile(model_bin_file):
 24 |             model_path = model_size
 25 | 
 26 |         logger.info(
 27 |             f"loading model: {model_path}, device: {device}, compute_type: {compute_type}"
 28 |         )
 29 |         try:
 30 |             model = WhisperModel(
 31 |                 model_size_or_path=model_path, device=device, compute_type=compute_type
 32 |             )
 33 |         except Exception as e:
 34 |             logger.error(
 35 |                 f"failed to load model: {e} \n\n"
 36 |                 f"********************************************\n"
 37 |                 f"this may be caused by network issue. \n"
 38 |                 f"please download the model manually and put it in the 'models' folder. \n"
 39 |                 f"see [README.md FAQ](https://github.com/harry0703/MoneyPrinterTurbo) for more details.\n"
 40 |                 f"********************************************\n\n"
 41 |             )
 42 |             return None
 43 | 
 44 |     logger.info(f"start, output file: {subtitle_file}")
 45 |     if not subtitle_file:
 46 |         subtitle_file = f"{audio_file}.srt"
 47 | 
 48 |     segments, info = model.transcribe(
 49 |         audio_file,
 50 |         beam_size=5,
 51 |         word_timestamps=True,
 52 |         vad_filter=True,
 53 |         vad_parameters=dict(min_silence_duration_ms=500),
 54 |     )
 55 | 
 56 |     logger.info(
 57 |         f"detected language: '{info.language}', probability: {info.language_probability:.2f}"
 58 |     )
 59 | 
 60 |     start = timer()
 61 |     subtitles = []
 62 | 
 63 |     def recognized(seg_text, seg_start, seg_end):
 64 |         seg_text = seg_text.strip()
 65 |         if not seg_text:
 66 |             return
 67 | 
 68 |         msg = "[%.2fs -> %.2fs] %s" % (seg_start, seg_end, seg_text)
 69 |         logger.debug(msg)
 70 | 
 71 |         subtitles.append(
 72 |             {"msg": seg_text, "start_time": seg_start, "end_time": seg_end}
 73 |         )
 74 | 
 75 |     for segment in segments:
 76 |         words_idx = 0
 77 |         words_len = len(segment.words)
 78 | 
 79 |         seg_start = 0
 80 |         seg_end = 0
 81 |         seg_text = ""
 82 | 
 83 |         if segment.words:
 84 |             is_segmented = False
 85 |             for word in segment.words:
 86 |                 if not is_segmented:
 87 |                     seg_start = word.start
 88 |                     is_segmented = True
 89 | 
 90 |                 seg_end = word.end
 91 |                 # If it contains punctuation, then break the sentence.
 92 |                 seg_text += word.word
 93 | 
 94 |                 if utils.str_contains_punctuation(word.word):
 95 |                     # remove last char
 96 |                     seg_text = seg_text[:-1]
 97 |                     if not seg_text:
 98 |                         continue
 99 | 
100 |                     recognized(seg_text, seg_start, seg_end)
101 | 
102 |                     is_segmented = False
103 |                     seg_text = ""
104 | 
105 |                 if words_idx == 0 and segment.start < word.start:
106 |                     seg_start = word.start
107 |                 if words_idx == (words_len - 1) and segment.end > word.end:
108 |                     seg_end = word.end
109 |                 words_idx += 1
110 | 
111 |         if not seg_text:
112 |             continue
113 | 
114 |         recognized(seg_text, seg_start, seg_end)
115 | 
116 |     end = timer()
117 | 
118 |     diff = end - start
119 |     logger.info(f"complete, elapsed: {diff:.2f} s")
120 | 
121 |     idx = 1
122 |     lines = []
123 |     for subtitle in subtitles:
124 |         text = subtitle.get("msg")
125 |         if text:
126 |             lines.append(
127 |                 utils.text_to_srt(
128 |                     idx, text, subtitle.get("start_time"), subtitle.get("end_time")
129 |                 )
130 |             )
131 |             idx += 1
132 | 
133 |     sub = "\n".join(lines) + "\n"
134 |     with open(subtitle_file, "w", encoding="utf-8") as f:
135 |         f.write(sub)
136 |     logger.info(f"subtitle file created: {subtitle_file}")
137 | 
138 | 
139 | def file_to_subtitles(filename):
140 |     if not filename or not os.path.isfile(filename):
141 |         return []
142 | 
143 |     times_texts = []
144 |     current_times = None
145 |     current_text = ""
146 |     index = 0
147 |     with open(filename, "r", encoding="utf-8") as f:
148 |         for line in f:
149 |             times = re.findall("([0-9]*:[0-9]*:[0-9]*,[0-9]*)", line)
150 |             if times:
151 |                 current_times = line
152 |             elif line.strip() == "" and current_times:
153 |                 index += 1
154 |                 times_texts.append((index, current_times.strip(), current_text.strip()))
155 |                 current_times, current_text = None, ""
156 |             elif current_times:
157 |                 current_text += line
158 |     return times_texts
159 | 
160 | 
161 | def levenshtein_distance(s1, s2):
162 |     if len(s1) < len(s2):
163 |         return levenshtein_distance(s2, s1)
164 | 
165 |     if len(s2) == 0:
166 |         return len(s1)
167 | 
168 |     previous_row = range(len(s2) + 1)
169 |     for i, c1 in enumerate(s1):
170 |         current_row = [i + 1]
171 |         for j, c2 in enumerate(s2):
172 |             insertions = previous_row[j + 1] + 1
173 |             deletions = current_row[j] + 1
174 |             substitutions = previous_row[j] + (c1 != c2)
175 |             current_row.append(min(insertions, deletions, substitutions))
176 |         previous_row = current_row
177 | 
178 |     return previous_row[-1]
179 | 
180 | 
181 | def similarity(a, b):
182 |     distance = levenshtein_distance(a.lower(), b.lower())
183 |     max_length = max(len(a), len(b))
184 |     return 1 - (distance / max_length)
185 | 
186 | 
187 | def correct(subtitle_file, video_script):
188 |     subtitle_items = file_to_subtitles(subtitle_file)
189 |     script_lines = utils.split_string_by_punctuations(video_script)
190 | 
191 |     corrected = False
192 |     new_subtitle_items = []
193 |     script_index = 0
194 |     subtitle_index = 0
195 | 
196 |     while script_index < len(script_lines) and subtitle_index < len(subtitle_items):
197 |         script_line = script_lines[script_index].strip()
198 |         subtitle_line = subtitle_items[subtitle_index][2].strip()
199 | 
200 |         if script_line == subtitle_line:
201 |             new_subtitle_items.append(subtitle_items[subtitle_index])
202 |             script_index += 1
203 |             subtitle_index += 1
204 |         else:
205 |             combined_subtitle = subtitle_line
206 |             start_time = subtitle_items[subtitle_index][1].split(" --> ")[0]
207 |             end_time = subtitle_items[subtitle_index][1].split(" --> ")[1]
208 |             next_subtitle_index = subtitle_index + 1
209 | 
210 |             while next_subtitle_index < len(subtitle_items):
211 |                 next_subtitle = subtitle_items[next_subtitle_index][2].strip()
212 |                 if similarity(
213 |                     script_line, combined_subtitle + " " + next_subtitle
214 |                 ) > similarity(script_line, combined_subtitle):
215 |                     combined_subtitle += " " + next_subtitle
216 |                     end_time = subtitle_items[next_subtitle_index][1].split(" --> ")[1]
217 |                     next_subtitle_index += 1
218 |                 else:
219 |                     break
220 | 
221 |             if similarity(script_line, combined_subtitle) > 0.8:
222 |                 logger.warning(
223 |                     f"Merged/Corrected - Script: {script_line}, Subtitle: {combined_subtitle}"
224 |                 )
225 |                 new_subtitle_items.append(
226 |                     (
227 |                         len(new_subtitle_items) + 1,
228 |                         f"{start_time} --> {end_time}",
229 |                         script_line,
230 |                     )
231 |                 )
232 |                 corrected = True
233 |             else:
234 |                 logger.warning(
235 |                     f"Mismatch - Script: {script_line}, Subtitle: {combined_subtitle}"
236 |                 )
237 |                 new_subtitle_items.append(
238 |                     (
239 |                         len(new_subtitle_items) + 1,
240 |                         f"{start_time} --> {end_time}",
241 |                         script_line,
242 |                     )
243 |                 )
244 |                 corrected = True
245 | 
246 |             script_index += 1
247 |             subtitle_index = next_subtitle_index
248 | 
249 |     # Process the remaining lines of the script.
250 |     while script_index < len(script_lines):
251 |         logger.warning(f"Extra script line: {script_lines[script_index]}")
252 |         if subtitle_index < len(subtitle_items):
253 |             new_subtitle_items.append(
254 |                 (
255 |                     len(new_subtitle_items) + 1,
256 |                     subtitle_items[subtitle_index][1],
257 |                     script_lines[script_index],
258 |                 )
259 |             )
260 |             subtitle_index += 1
261 |         else:
262 |             new_subtitle_items.append(
263 |                 (
264 |                     len(new_subtitle_items) + 1,
265 |                     "00:00:00,000 --> 00:00:00,000",
266 |                     script_lines[script_index],
267 |                 )
268 |             )
269 |         script_index += 1
270 |         corrected = True
271 | 
272 |     if corrected:
273 |         with open(subtitle_file, "w", encoding="utf-8") as fd:
274 |             for i, item in enumerate(new_subtitle_items):
275 |                 fd.write(f"{i + 1}\n{item[1]}\n{item[2]}\n\n")
276 |         logger.info("Subtitle corrected")
277 |     else:
278 |         logger.success("Subtitle is correct")
279 | 
280 | 
281 | if __name__ == "__main__":
282 |     task_id = "c12fd1e6-4b0a-4d65-a075-c87abe35a072"
283 |     task_dir = utils.task_dir(task_id)
284 |     subtitle_file = f"{task_dir}/subtitle.srt"
285 |     audio_file = f"{task_dir}/audio.mp3"
286 | 
287 |     subtitles = file_to_subtitles(subtitle_file)
288 |     print(subtitles)
289 | 
290 |     script_file = f"{task_dir}/script.json"
291 |     with open(script_file, "r") as f:
292 |         script_content = f.read()
293 |     s = json.loads(script_content)
294 |     script = s.get("script")
295 | 
296 |     correct(subtitle_file, script)
297 | 
298 |     subtitle_file = f"{task_dir}/subtitle-test.srt"
299 |     create(audio_file, subtitle_file)
300 | 


--------------------------------------------------------------------------------
/app/services/task.py:
--------------------------------------------------------------------------------
  1 | import math
  2 | import os.path
  3 | import re
  4 | from os import path
  5 | 
  6 | from loguru import logger
  7 | 
  8 | from app.config import config
  9 | from app.models import const
 10 | from app.models.schema import VideoConcatMode, VideoParams
 11 | from app.services import llm, material, subtitle, video, voice
 12 | from app.services import state as sm
 13 | from app.utils import utils
 14 | 
 15 | 
 16 | def generate_script(task_id, params):
 17 |     logger.info("\n\n## generating video script")
 18 |     video_script = params.video_script.strip()
 19 |     if not video_script:
 20 |         video_script = llm.generate_script(
 21 |             video_subject=params.video_subject,
 22 |             language=params.video_language,
 23 |             paragraph_number=params.paragraph_number,
 24 |         )
 25 |     else:
 26 |         logger.debug(f"video script: \n{video_script}")
 27 | 
 28 |     if not video_script:
 29 |         sm.state.update_task(task_id, state=const.TASK_STATE_FAILED)
 30 |         logger.error("failed to generate video script.")
 31 |         return None
 32 | 
 33 |     return video_script
 34 | 
 35 | 
 36 | def generate_terms(task_id, params, video_script):
 37 |     logger.info("\n\n## generating video terms")
 38 |     video_terms = params.video_terms
 39 |     if not video_terms:
 40 |         video_terms = llm.generate_terms(
 41 |             video_subject=params.video_subject, video_script=video_script, amount=5
 42 |         )
 43 |     else:
 44 |         if isinstance(video_terms, str):
 45 |             video_terms = [term.strip() for term in re.split(r"[,,]", video_terms)]
 46 |         elif isinstance(video_terms, list):
 47 |             video_terms = [term.strip() for term in video_terms]
 48 |         else:
 49 |             raise ValueError("video_terms must be a string or a list of strings.")
 50 | 
 51 |         logger.debug(f"video terms: {utils.to_json(video_terms)}")
 52 | 
 53 |     if not video_terms:
 54 |         sm.state.update_task(task_id, state=const.TASK_STATE_FAILED)
 55 |         logger.error("failed to generate video terms.")
 56 |         return None
 57 | 
 58 |     return video_terms
 59 | 
 60 | 
 61 | def save_script_data(task_id, video_script, video_terms, params):
 62 |     script_file = path.join(utils.task_dir(task_id), "script.json")
 63 |     script_data = {
 64 |         "script": video_script,
 65 |         "search_terms": video_terms,
 66 |         "params": params,
 67 |     }
 68 | 
 69 |     with open(script_file, "w", encoding="utf-8") as f:
 70 |         f.write(utils.to_json(script_data))
 71 | 
 72 | 
 73 | def generate_audio(task_id, params, video_script):
 74 |     logger.info("\n\n## generating audio")
 75 |     audio_file = path.join(utils.task_dir(task_id), "audio.mp3")
 76 |     sub_maker = voice.tts(
 77 |         text=video_script,
 78 |         voice_name=voice.parse_voice_name(params.voice_name),
 79 |         voice_rate=params.voice_rate,
 80 |         voice_file=audio_file,
 81 |     )
 82 |     if sub_maker is None:
 83 |         sm.state.update_task(task_id, state=const.TASK_STATE_FAILED)
 84 |         logger.error(
 85 |             """failed to generate audio:
 86 | 1. check if the language of the voice matches the language of the video script.
 87 | 2. check if the network is available. If you are in China, it is recommended to use a VPN and enable the global traffic mode.
 88 |         """.strip()
 89 |         )
 90 |         return None, None, None
 91 | 
 92 |     audio_duration = math.ceil(voice.get_audio_duration(sub_maker))
 93 |     return audio_file, audio_duration, sub_maker
 94 | 
 95 | 
 96 | def generate_subtitle(task_id, params, video_script, sub_maker, audio_file):
 97 |     if not params.subtitle_enabled:
 98 |         return ""
 99 | 
100 |     subtitle_path = path.join(utils.task_dir(task_id), "subtitle.srt")
101 |     subtitle_provider = config.app.get("subtitle_provider", "edge").strip().lower()
102 |     logger.info(f"\n\n## generating subtitle, provider: {subtitle_provider}")
103 | 
104 |     subtitle_fallback = False
105 |     if subtitle_provider == "edge":
106 |         voice.create_subtitle(
107 |             text=video_script, sub_maker=sub_maker, subtitle_file=subtitle_path
108 |         )
109 |         if not os.path.exists(subtitle_path):
110 |             subtitle_fallback = True
111 |             logger.warning("subtitle file not found, fallback to whisper")
112 | 
113 |     if subtitle_provider == "whisper" or subtitle_fallback:
114 |         subtitle.create(audio_file=audio_file, subtitle_file=subtitle_path)
115 |         logger.info("\n\n## correcting subtitle")
116 |         subtitle.correct(subtitle_file=subtitle_path, video_script=video_script)
117 | 
118 |     subtitle_lines = subtitle.file_to_subtitles(subtitle_path)
119 |     if not subtitle_lines:
120 |         logger.warning(f"subtitle file is invalid: {subtitle_path}")
121 |         return ""
122 | 
123 |     return subtitle_path
124 | 
125 | 
126 | def get_video_materials(task_id, params, video_terms, audio_duration):
127 |     if params.video_source == "local":
128 |         logger.info("\n\n## preprocess local materials")
129 |         materials = video.preprocess_video(
130 |             materials=params.video_materials, clip_duration=params.video_clip_duration
131 |         )
132 |         if not materials:
133 |             sm.state.update_task(task_id, state=const.TASK_STATE_FAILED)
134 |             logger.error(
135 |                 "no valid materials found, please check the materials and try again."
136 |             )
137 |             return None
138 |         return [material_info.url for material_info in materials]
139 |     else:
140 |         logger.info(f"\n\n## downloading videos from {params.video_source}")
141 |         downloaded_videos = material.download_videos(
142 |             task_id=task_id,
143 |             search_terms=video_terms,
144 |             source=params.video_source,
145 |             video_aspect=params.video_aspect,
146 |             video_contact_mode=params.video_concat_mode,
147 |             audio_duration=audio_duration * params.video_count,
148 |             max_clip_duration=params.video_clip_duration,
149 |         )
150 |         if not downloaded_videos:
151 |             sm.state.update_task(task_id, state=const.TASK_STATE_FAILED)
152 |             logger.error(
153 |                 "failed to download videos, maybe the network is not available. if you are in China, please use a VPN."
154 |             )
155 |             return None
156 |         return downloaded_videos
157 | 
158 | 
159 | def generate_final_videos(
160 |     task_id, params, downloaded_videos, audio_file, subtitle_path
161 | ):
162 |     final_video_paths = []
163 |     combined_video_paths = []
164 |     video_concat_mode = (
165 |         params.video_concat_mode if params.video_count == 1 else VideoConcatMode.random
166 |     )
167 |     video_transition_mode = params.video_transition_mode
168 | 
169 |     _progress = 50
170 |     for i in range(params.video_count):
171 |         index = i + 1
172 |         combined_video_path = path.join(
173 |             utils.task_dir(task_id), f"combined-{index}.mp4"
174 |         )
175 |         logger.info(f"\n\n## combining video: {index} => {combined_video_path}")
176 |         video.combine_videos(
177 |             combined_video_path=combined_video_path,
178 |             video_paths=downloaded_videos,
179 |             audio_file=audio_file,
180 |             video_aspect=params.video_aspect,
181 |             video_concat_mode=video_concat_mode,
182 |             video_transition_mode=video_transition_mode,
183 |             max_clip_duration=params.video_clip_duration,
184 |             threads=params.n_threads,
185 |         )
186 | 
187 |         _progress += 50 / params.video_count / 2
188 |         sm.state.update_task(task_id, progress=_progress)
189 | 
190 |         final_video_path = path.join(utils.task_dir(task_id), f"final-{index}.mp4")
191 | 
192 |         logger.info(f"\n\n## generating video: {index} => {final_video_path}")
193 |         video.generate_video(
194 |             video_path=combined_video_path,
195 |             audio_path=audio_file,
196 |             subtitle_path=subtitle_path,
197 |             output_file=final_video_path,
198 |             params=params,
199 |         )
200 | 
201 |         _progress += 50 / params.video_count / 2
202 |         sm.state.update_task(task_id, progress=_progress)
203 | 
204 |         final_video_paths.append(final_video_path)
205 |         combined_video_paths.append(combined_video_path)
206 | 
207 |     return final_video_paths, combined_video_paths
208 | 
209 | 
210 | def start(task_id, params: VideoParams, stop_at: str = "video"):
211 |     logger.info(f"start task: {task_id}, stop_at: {stop_at}")
212 |     sm.state.update_task(task_id, state=const.TASK_STATE_PROCESSING, progress=5)
213 | 
214 |     if type(params.video_concat_mode) is str:
215 |         params.video_concat_mode = VideoConcatMode(params.video_concat_mode)
216 | 
217 |     # 1. Generate script
218 |     video_script = generate_script(task_id, params)
219 |     if not video_script or "Error: " in video_script:
220 |         sm.state.update_task(task_id, state=const.TASK_STATE_FAILED)
221 |         return
222 | 
223 |     sm.state.update_task(task_id, state=const.TASK_STATE_PROCESSING, progress=10)
224 | 
225 |     if stop_at == "script":
226 |         sm.state.update_task(
227 |             task_id, state=const.TASK_STATE_COMPLETE, progress=100, script=video_script
228 |         )
229 |         return {"script": video_script}
230 | 
231 |     # 2. Generate terms
232 |     video_terms = ""
233 |     if params.video_source != "local":
234 |         video_terms = generate_terms(task_id, params, video_script)
235 |         if not video_terms:
236 |             sm.state.update_task(task_id, state=const.TASK_STATE_FAILED)
237 |             return
238 | 
239 |     save_script_data(task_id, video_script, video_terms, params)
240 | 
241 |     if stop_at == "terms":
242 |         sm.state.update_task(
243 |             task_id, state=const.TASK_STATE_COMPLETE, progress=100, terms=video_terms
244 |         )
245 |         return {"script": video_script, "terms": video_terms}
246 | 
247 |     sm.state.update_task(task_id, state=const.TASK_STATE_PROCESSING, progress=20)
248 | 
249 |     # 3. Generate audio
250 |     audio_file, audio_duration, sub_maker = generate_audio(
251 |         task_id, params, video_script
252 |     )
253 |     if not audio_file:
254 |         sm.state.update_task(task_id, state=const.TASK_STATE_FAILED)
255 |         return
256 | 
257 |     sm.state.update_task(task_id, state=const.TASK_STATE_PROCESSING, progress=30)
258 | 
259 |     if stop_at == "audio":
260 |         sm.state.update_task(
261 |             task_id,
262 |             state=const.TASK_STATE_COMPLETE,
263 |             progress=100,
264 |             audio_file=audio_file,
265 |         )
266 |         return {"audio_file": audio_file, "audio_duration": audio_duration}
267 | 
268 |     # 4. Generate subtitle
269 |     subtitle_path = generate_subtitle(
270 |         task_id, params, video_script, sub_maker, audio_file
271 |     )
272 | 
273 |     if stop_at == "subtitle":
274 |         sm.state.update_task(
275 |             task_id,
276 |             state=const.TASK_STATE_COMPLETE,
277 |             progress=100,
278 |             subtitle_path=subtitle_path,
279 |         )
280 |         return {"subtitle_path": subtitle_path}
281 | 
282 |     sm.state.update_task(task_id, state=const.TASK_STATE_PROCESSING, progress=40)
283 | 
284 |     # 5. Get video materials
285 |     downloaded_videos = get_video_materials(
286 |         task_id, params, video_terms, audio_duration
287 |     )
288 |     if not downloaded_videos:
289 |         sm.state.update_task(task_id, state=const.TASK_STATE_FAILED)
290 |         return
291 | 
292 |     if stop_at == "materials":
293 |         sm.state.update_task(
294 |             task_id,
295 |             state=const.TASK_STATE_COMPLETE,
296 |             progress=100,
297 |             materials=downloaded_videos,
298 |         )
299 |         return {"materials": downloaded_videos}
300 | 
301 |     sm.state.update_task(task_id, state=const.TASK_STATE_PROCESSING, progress=50)
302 | 
303 |     # 6. Generate final videos
304 |     final_video_paths, combined_video_paths = generate_final_videos(
305 |         task_id, params, downloaded_videos, audio_file, subtitle_path
306 |     )
307 | 
308 |     if not final_video_paths:
309 |         sm.state.update_task(task_id, state=const.TASK_STATE_FAILED)
310 |         return
311 | 
312 |     logger.success(
313 |         f"task {task_id} finished, generated {len(final_video_paths)} videos."
314 |     )
315 | 
316 |     kwargs = {
317 |         "videos": final_video_paths,
318 |         "combined_videos": combined_video_paths,
319 |         "script": video_script,
320 |         "terms": video_terms,
321 |         "audio_file": audio_file,
322 |         "audio_duration": audio_duration,
323 |         "subtitle_path": subtitle_path,
324 |         "materials": downloaded_videos,
325 |     }
326 |     sm.state.update_task(
327 |         task_id, state=const.TASK_STATE_COMPLETE, progress=100, **kwargs
328 |     )
329 |     return kwargs
330 | 
331 | 
332 | if __name__ == "__main__":
333 |     task_id = "task_id"
334 |     params = VideoParams(
335 |         video_subject="金钱的作用",
336 |         voice_name="zh-CN-XiaoyiNeural-Female",
337 |         voice_rate=1.0,
338 |     )
339 |     start(task_id, params, stop_at="video")
340 | 


--------------------------------------------------------------------------------
/app/services/utils/video_effects.py:
--------------------------------------------------------------------------------
 1 | from moviepy import Clip, vfx
 2 | 
 3 | 
 4 | # FadeIn
 5 | def fadein_transition(clip: Clip, t: float) -> Clip:
 6 |     return clip.with_effects([vfx.FadeIn(t)])
 7 | 
 8 | 
 9 | # FadeOut
10 | def fadeout_transition(clip: Clip, t: float) -> Clip:
11 |     return clip.with_effects([vfx.FadeOut(t)])
12 | 
13 | 
14 | # SlideIn
15 | def slidein_transition(clip: Clip, t: float, side: str) -> Clip:
16 |     return clip.with_effects([vfx.SlideIn(t, side)])
17 | 
18 | 
19 | # SlideOut
20 | def slideout_transition(clip: Clip, t: float, side: str) -> Clip:
21 |     return clip.with_effects([vfx.SlideOut(t, side)])
22 | 


--------------------------------------------------------------------------------
/app/utils/utils.py:
--------------------------------------------------------------------------------
  1 | import json
  2 | import locale
  3 | import os
  4 | from pathlib import Path
  5 | import threading
  6 | from typing import Any
  7 | from uuid import uuid4
  8 | 
  9 | import urllib3
 10 | from loguru import logger
 11 | 
 12 | from app.models import const
 13 | 
 14 | urllib3.disable_warnings()
 15 | 
 16 | 
 17 | def get_response(status: int, data: Any = None, message: str = ""):
 18 |     obj = {
 19 |         "status": status,
 20 |     }
 21 |     if data:
 22 |         obj["data"] = data
 23 |     if message:
 24 |         obj["message"] = message
 25 |     return obj
 26 | 
 27 | 
 28 | def to_json(obj):
 29 |     try:
 30 |         # Define a helper function to handle different types of objects
 31 |         def serialize(o):
 32 |             # If the object is a serializable type, return it directly
 33 |             if isinstance(o, (int, float, bool, str)) or o is None:
 34 |                 return o
 35 |             # If the object is binary data, convert it to a base64-encoded string
 36 |             elif isinstance(o, bytes):
 37 |                 return "*** binary data ***"
 38 |             # If the object is a dictionary, recursively process each key-value pair
 39 |             elif isinstance(o, dict):
 40 |                 return {k: serialize(v) for k, v in o.items()}
 41 |             # If the object is a list or tuple, recursively process each element
 42 |             elif isinstance(o, (list, tuple)):
 43 |                 return [serialize(item) for item in o]
 44 |             # If the object is a custom type, attempt to return its __dict__ attribute
 45 |             elif hasattr(o, "__dict__"):
 46 |                 return serialize(o.__dict__)
 47 |             # Return None for other cases (or choose to raise an exception)
 48 |             else:
 49 |                 return None
 50 | 
 51 |         # Use the serialize function to process the input object
 52 |         serialized_obj = serialize(obj)
 53 | 
 54 |         # Serialize the processed object into a JSON string
 55 |         return json.dumps(serialized_obj, ensure_ascii=False, indent=4)
 56 |     except Exception:
 57 |         return None
 58 | 
 59 | 
 60 | def get_uuid(remove_hyphen: bool = False):
 61 |     u = str(uuid4())
 62 |     if remove_hyphen:
 63 |         u = u.replace("-", "")
 64 |     return u
 65 | 
 66 | 
 67 | def root_dir():
 68 |     return os.path.dirname(os.path.dirname(os.path.dirname(os.path.realpath(__file__))))
 69 | 
 70 | 
 71 | def storage_dir(sub_dir: str = "", create: bool = False):
 72 |     d = os.path.join(root_dir(), "storage")
 73 |     if sub_dir:
 74 |         d = os.path.join(d, sub_dir)
 75 |     if create and not os.path.exists(d):
 76 |         os.makedirs(d)
 77 | 
 78 |     return d
 79 | 
 80 | 
 81 | def resource_dir(sub_dir: str = ""):
 82 |     d = os.path.join(root_dir(), "resource")
 83 |     if sub_dir:
 84 |         d = os.path.join(d, sub_dir)
 85 |     return d
 86 | 
 87 | 
 88 | def task_dir(sub_dir: str = ""):
 89 |     d = os.path.join(storage_dir(), "tasks")
 90 |     if sub_dir:
 91 |         d = os.path.join(d, sub_dir)
 92 |     if not os.path.exists(d):
 93 |         os.makedirs(d)
 94 |     return d
 95 | 
 96 | 
 97 | def font_dir(sub_dir: str = ""):
 98 |     d = resource_dir("fonts")
 99 |     if sub_dir:
100 |         d = os.path.join(d, sub_dir)
101 |     if not os.path.exists(d):
102 |         os.makedirs(d)
103 |     return d
104 | 
105 | 
106 | def song_dir(sub_dir: str = ""):
107 |     d = resource_dir("songs")
108 |     if sub_dir:
109 |         d = os.path.join(d, sub_dir)
110 |     if not os.path.exists(d):
111 |         os.makedirs(d)
112 |     return d
113 | 
114 | 
115 | def public_dir(sub_dir: str = ""):
116 |     d = resource_dir("public")
117 |     if sub_dir:
118 |         d = os.path.join(d, sub_dir)
119 |     if not os.path.exists(d):
120 |         os.makedirs(d)
121 |     return d
122 | 
123 | 
124 | def run_in_background(func, *args, **kwargs):
125 |     def run():
126 |         try:
127 |             func(*args, **kwargs)
128 |         except Exception as e:
129 |             logger.error(f"run_in_background error: {e}")
130 | 
131 |     thread = threading.Thread(target=run)
132 |     thread.start()
133 |     return thread
134 | 
135 | 
136 | def time_convert_seconds_to_hmsm(seconds) -> str:
137 |     hours = int(seconds // 3600)
138 |     seconds = seconds % 3600
139 |     minutes = int(seconds // 60)
140 |     milliseconds = int(seconds * 1000) % 1000
141 |     seconds = int(seconds % 60)
142 |     return "{:02d}:{:02d}:{:02d},{:03d}".format(hours, minutes, seconds, milliseconds)
143 | 
144 | 
145 | def text_to_srt(idx: int, msg: str, start_time: float, end_time: float) -> str:
146 |     start_time = time_convert_seconds_to_hmsm(start_time)
147 |     end_time = time_convert_seconds_to_hmsm(end_time)
148 |     srt = """%d
149 | %s --> %s
150 | %s
151 |         """ % (
152 |         idx,
153 |         start_time,
154 |         end_time,
155 |         msg,
156 |     )
157 |     return srt
158 | 
159 | 
160 | def str_contains_punctuation(word):
161 |     for p in const.PUNCTUATIONS:
162 |         if p in word:
163 |             return True
164 |     return False
165 | 
166 | 
167 | def split_string_by_punctuations(s):
168 |     result = []
169 |     txt = ""
170 | 
171 |     previous_char = ""
172 |     next_char = ""
173 |     for i in range(len(s)):
174 |         char = s[i]
175 |         if char == "\n":
176 |             result.append(txt.strip())
177 |             txt = ""
178 |             continue
179 | 
180 |         if i > 0:
181 |             previous_char = s[i - 1]
182 |         if i < len(s) - 1:
183 |             next_char = s[i + 1]
184 | 
185 |         if char == "." and previous_char.isdigit() and next_char.isdigit():
186 |             # # In the case of "withdraw 10,000, charged at 2.5% fee", the dot in "2.5" should not be treated as a line break marker
187 |             txt += char
188 |             continue
189 | 
190 |         if char not in const.PUNCTUATIONS:
191 |             txt += char
192 |         else:
193 |             result.append(txt.strip())
194 |             txt = ""
195 |     result.append(txt.strip())
196 |     # filter empty string
197 |     result = list(filter(None, result))
198 |     return result
199 | 
200 | 
201 | def md5(text):
202 |     import hashlib
203 | 
204 |     return hashlib.md5(text.encode("utf-8")).hexdigest()
205 | 
206 | 
207 | def get_system_locale():
208 |     try:
209 |         loc = locale.getdefaultlocale()
210 |         # zh_CN, zh_TW return zh
211 |         # en_US, en_GB return en
212 |         language_code = loc[0].split("_")[0]
213 |         return language_code
214 |     except Exception:
215 |         return "en"
216 | 
217 | 
218 | def load_locales(i18n_dir):
219 |     _locales = {}
220 |     for root, dirs, files in os.walk(i18n_dir):
221 |         for file in files:
222 |             if file.endswith(".json"):
223 |                 lang = file.split(".")[0]
224 |                 with open(os.path.join(root, file), "r", encoding="utf-8") as f:
225 |                     _locales[lang] = json.loads(f.read())
226 |     return _locales
227 | 
228 | 
229 | def parse_extension(filename):
230 |     return Path(filename).suffix.lower().lstrip('.')
231 | 


--------------------------------------------------------------------------------
/config.example.toml:
--------------------------------------------------------------------------------
  1 | [app]
  2 | video_source = "pexels" # "pexels" or "pixabay"
  3 | 
  4 | # 是否隐藏配置面板
  5 | hide_config = false
  6 | 
  7 | # Pexels API Key
  8 | # Register at https://www.pexels.com/api/ to get your API key.
  9 | # You can use multiple keys to avoid rate limits.
 10 | # For example: pexels_api_keys = ["123adsf4567adf89","abd1321cd13efgfdfhi"]
 11 | # 特别注意格式,Key 用英文双引号括起来,多个Key用逗号隔开
 12 | pexels_api_keys = []
 13 | 
 14 | # Pixabay API Key
 15 | # Register at https://pixabay.com/api/docs/ to get your API key.
 16 | # You can use multiple keys to avoid rate limits.
 17 | # For example: pixabay_api_keys = ["123adsf4567adf89","abd1321cd13efgfdfhi"]
 18 | # 特别注意格式,Key 用英文双引号括起来,多个Key用逗号隔开
 19 | pixabay_api_keys = []
 20 | 
 21 | # 支持的提供商 (Supported providers):
 22 | #   openai
 23 | #   moonshot    (月之暗面)
 24 | #   azure
 25 | #   qwen        (通义千问)
 26 | #   deepseek
 27 | #   gemini
 28 | #   ollama
 29 | #   g4f
 30 | #   oneapi
 31 | #   cloudflare
 32 | #   ernie       (文心一言)
 33 | llm_provider = "openai"
 34 | 
 35 | ########## Pollinations AI Settings
 36 | # Visit https://pollinations.ai/ to learn more
 37 | # API Key is optional - leave empty for public access
 38 | pollinations_api_key = ""
 39 | # Default base URL for Pollinations API
 40 | pollinations_base_url = "https://pollinations.ai/api/v1"
 41 | # Default model for text generation
 42 | pollinations_model_name = "openai-fast"
 43 | 
 44 | ########## Ollama Settings
 45 | # No need to set it unless you want to use your own proxy
 46 | ollama_base_url = ""
 47 | # Check your available models at https://ollama.com/library
 48 | ollama_model_name = ""
 49 | 
 50 | ########## OpenAI API Key
 51 | # Get your API key at https://platform.openai.com/api-keys
 52 | openai_api_key = ""
 53 | # No need to set it unless you want to use your own proxy
 54 | openai_base_url = ""
 55 | # Check your available models at https://platform.openai.com/account/limits
 56 | openai_model_name = "gpt-4o-mini"
 57 | 
 58 | ########## Moonshot API Key
 59 | # Visit https://platform.moonshot.cn/console/api-keys to get your API key.
 60 | moonshot_api_key = ""
 61 | moonshot_base_url = "https://api.moonshot.cn/v1"
 62 | moonshot_model_name = "moonshot-v1-8k"
 63 | 
 64 | ########## OneAPI API Key
 65 | # Visit https://github.com/songquanpeng/one-api to get your API key
 66 | oneapi_api_key = ""
 67 | oneapi_base_url = ""
 68 | oneapi_model_name = ""
 69 | 
 70 | ########## G4F
 71 | # Visit https://github.com/xtekky/gpt4free to get more details
 72 | # Supported model list: https://github.com/xtekky/gpt4free/blob/main/g4f/models.py
 73 | g4f_model_name = "gpt-3.5-turbo"
 74 | 
 75 | ########## Azure API Key
 76 | # Visit https://learn.microsoft.com/zh-cn/azure/ai-services/openai/ to get more details
 77 | # API documentation: https://learn.microsoft.com/zh-cn/azure/ai-services/openai/reference
 78 | azure_api_key = ""
 79 | azure_base_url = ""
 80 | azure_model_name = "gpt-35-turbo"        # replace with your model deployment name
 81 | azure_api_version = "2024-02-15-preview"
 82 | 
 83 | ########## Gemini API Key
 84 | gemini_api_key = ""
 85 | gemini_model_name = "gemini-1.0-pro"
 86 | 
 87 | ########## Qwen API Key
 88 | # Visit https://dashscope.console.aliyun.com/apiKey to get your API key
 89 | # Visit below links to get more details
 90 | # https://tongyi.aliyun.com/qianwen/
 91 | # https://help.aliyun.com/zh/dashscope/developer-reference/model-introduction
 92 | qwen_api_key = ""
 93 | qwen_model_name = "qwen-max"
 94 | 
 95 | 
 96 | ########## DeepSeek API Key
 97 | # Visit https://platform.deepseek.com/api_keys to get your API key
 98 | deepseek_api_key = ""
 99 | deepseek_base_url = "https://api.deepseek.com"
100 | deepseek_model_name = "deepseek-chat"
101 | 
102 | # Subtitle Provider, "edge" or "whisper"
103 | # If empty, the subtitle will not be generated
104 | subtitle_provider = "edge"
105 | 
106 | #
107 | # ImageMagick
108 | #
109 | # Once you have installed it, ImageMagick will be automatically detected, except on Windows!
110 | # On Windows, for example "C:\Program Files (x86)\ImageMagick-7.1.1-Q16-HDRI\magick.exe"
111 | # Download from https://imagemagick.org/archive/binaries/ImageMagick-7.1.1-29-Q16-x64-static.exe
112 | 
113 | # imagemagick_path = "C:\\Program Files (x86)\\ImageMagick-7.1.1-Q16\\magick.exe"
114 | 
115 | 
116 | #
117 | # FFMPEG
118 | #
119 | # 通常情况下,ffmpeg 会被自动下载,并且会被自动检测到。
120 | # 但是如果你的环境有问题,无法自动下载,可能会遇到如下错误:
121 | #   RuntimeError: No ffmpeg exe could be found.
122 | #   Install ffmpeg on your system, or set the IMAGEIO_FFMPEG_EXE environment variable.
123 | # 此时你可以手动下载 ffmpeg 并设置 ffmpeg_path,下载地址:https://www.gyan.dev/ffmpeg/builds/
124 | 
125 | # Under normal circumstances, ffmpeg is downloaded automatically and detected automatically.
126 | # However, if there is an issue with your environment that prevents automatic downloading, you might encounter the following error:
127 | #   RuntimeError: No ffmpeg exe could be found.
128 | #   Install ffmpeg on your system, or set the IMAGEIO_FFMPEG_EXE environment variable.
129 | # In such cases, you can manually download ffmpeg and set the ffmpeg_path, download link: https://www.gyan.dev/ffmpeg/builds/
130 | 
131 | # ffmpeg_path = "C:\\Users\\harry\\Downloads\\ffmpeg.exe"
132 | #########################################################################################
133 | 
134 | # 当视频生成成功后,API服务提供的视频下载接入点,默认为当前服务的地址和监听端口
135 | # 比如 http://127.0.0.1:8080/tasks/6357f542-a4e1-46a1-b4c9-bf3bd0df5285/final-1.mp4
136 | # 如果你需要使用域名对外提供服务(一般会用nginx做代理),则可以设置为你的域名
137 | # 比如 https://xxxx.com/tasks/6357f542-a4e1-46a1-b4c9-bf3bd0df5285/final-1.mp4
138 | # endpoint="https://xxxx.com"
139 | 
140 | # 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.
141 | # For example, http://127.0.0.1:8080/tasks/6357f542-a4e1-46a1-b4c9-bf3bd0df5285/final-1.mp4
142 | # 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.
143 | # For example, https://xxxx.com/tasks/6357f542-a4e1-46a1-b4c9-bf3bd0df5285/final-1.mp4
144 | # endpoint="https://xxxx.com"
145 | endpoint = ""
146 | 
147 | 
148 | # Video material storage location
149 | # material_directory = ""                    # Indicates that video materials will be downloaded to the default folder, the default folder is ./storage/cache_videos under the current project
150 | # material_directory = "/user/harry/videos"  # Indicates that video materials will be downloaded to a specified folder
151 | # material_directory = "task"                # Indicates that video materials will be downloaded to the current task's folder, this method does not allow sharing of already downloaded video materials
152 | 
153 | # 视频素材存放位置
154 | # material_directory = ""                    #表示将视频素材下载到默认的文件夹,默认文件夹为当前项目下的 ./storage/cache_videos
155 | # material_directory = "/user/harry/videos"  #表示将视频素材下载到指定的文件夹中
156 | # material_directory = "task"                #表示将视频素材下载到当前任务的文件夹中,这种方式无法共享已经下载的视频素材
157 | 
158 | material_directory = ""
159 | 
160 | # Used for state management of the task
161 | enable_redis = false
162 | redis_host = "localhost"
163 | redis_port = 6379
164 | redis_db = 0
165 | redis_password = ""
166 | 
167 | # 文生视频时的最大并发任务数
168 | max_concurrent_tasks = 5
169 | 
170 | 
171 | [whisper]
172 | # Only effective when subtitle_provider is "whisper"
173 | 
174 | # Run on GPU with FP16
175 | # model = WhisperModel(model_size, device="cuda", compute_type="float16")
176 | 
177 | # Run on GPU with INT8
178 | # model = WhisperModel(model_size, device="cuda", compute_type="int8_float16")
179 | 
180 | # Run on CPU with INT8
181 | # model = WhisperModel(model_size, device="cpu", compute_type="int8")
182 | 
183 | # recommended model_size: "large-v3"
184 | model_size = "large-v3"
185 | # if you want to use GPU, set device="cuda"
186 | device = "CPU"
187 | compute_type = "int8"
188 | 
189 | 
190 | [proxy]
191 | ### Use a proxy to access the Pexels API
192 | ### Format: "http://<username>:<password>@<proxy>:<port>"
193 | ### Example: "http://user:pass@proxy:1234"
194 | ### Doc: https://requests.readthedocs.io/en/latest/user/advanced/#proxies
195 | 
196 | # http = "http://10.10.1.10:3128"
197 | # https = "http://10.10.1.10:1080"
198 | 
199 | [azure]
200 | # Azure Speech API Key
201 | # Get your API key at https://portal.azure.com/#view/Microsoft_Azure_ProjectOxford/CognitiveServicesHub/~/SpeechServices
202 | speech_key = ""
203 | speech_region = ""
204 | 
205 | [siliconflow]
206 | # SiliconFlow API Key
207 | # Get your API key at https://siliconflow.cn
208 | api_key = ""
209 | 
210 | [ui]
211 | # UI related settings
212 | # 是否隐藏日志信息
213 | # Whether to hide logs in the UI
214 | hide_log = false
215 | 


--------------------------------------------------------------------------------
/docker-compose.yml:
--------------------------------------------------------------------------------
 1 | x-common-volumes: &common-volumes
 2 |   - ./:/MoneyPrinterTurbo
 3 | 
 4 | services:
 5 |   webui:
 6 |     build:
 7 |       context: .
 8 |       dockerfile: Dockerfile
 9 |     container_name: "moneyprinterturbo-webui"
10 |     ports:
11 |       - "8501:8501"
12 |     command: [ "streamlit", "run", "./webui/Main.py","--browser.serverAddress=127.0.0.1","--server.enableCORS=True","--browser.gatherUsageStats=False" ]
13 |     volumes: *common-volumes
14 |     restart: always
15 |   api:
16 |     build:
17 |       context: .
18 |       dockerfile: Dockerfile
19 |     container_name: "moneyprinterturbo-api"
20 |     ports:
21 |       - "8080:8080"
22 |     command: [ "python3", "main.py" ]
23 |     volumes: *common-volumes
24 |     restart: always


--------------------------------------------------------------------------------
/docs/MoneyPrinterTurbo.ipynb:
--------------------------------------------------------------------------------
  1 | {
  2 |   "cells": [
  3 |     {
  4 |       "cell_type": "markdown",
  5 |       "metadata": {},
  6 |       "source": [
  7 |         "# MoneyPrinterTurbo Setup Guide\n",
  8 |         "\n",
  9 |         "This notebook will guide you through the process of setting up [MoneyPrinterTurbo](https://github.com/harry0703/MoneyPrinterTurbo)."
 10 |       ]
 11 |     },
 12 |     {
 13 |       "cell_type": "markdown",
 14 |       "metadata": {},
 15 |       "source": [
 16 |         "## 1. Clone Repository and Install Dependencies\n",
 17 |         "\n",
 18 |         "First, we'll clone the repository from GitHub and install all required packages:"
 19 |       ]
 20 |     },
 21 |     {
 22 |       "cell_type": "code",
 23 |       "execution_count": null,
 24 |       "metadata": {
 25 |         "id": "S8Eu-aQarY_B"
 26 |       },
 27 |       "outputs": [],
 28 |       "source": [
 29 |         "!git clone https://github.com/harry0703/MoneyPrinterTurbo.git\n",
 30 |         "%cd MoneyPrinterTurbo\n",
 31 |         "!pip install -q -r requirements.txt\n",
 32 |         "!pip install pyngrok --quiet"
 33 |       ]
 34 |     },
 35 |     {
 36 |       "cell_type": "markdown",
 37 |       "metadata": {},
 38 |       "source": [
 39 |         "## 2. Configure ngrok for Remote Access\n",
 40 |         "\n",
 41 |         "We'll use ngrok to create a secure tunnel to expose our local Streamlit server to the internet.\n",
 42 |         "\n",
 43 |         "**Important**: You need to get your authentication token from the [ngrok dashboard](https://dashboard.ngrok.com/get-started/your-authtoken) to use this service."
 44 |       ]
 45 |     },
 46 |     {
 47 |       "cell_type": "code",
 48 |       "execution_count": null,
 49 |       "metadata": {},
 50 |       "outputs": [],
 51 |       "source": [
 52 |         "from pyngrok import ngrok\n",
 53 |         "\n",
 54 |         "# Terminate any existing ngrok tunnels\n",
 55 |         "ngrok.kill()\n",
 56 |         "\n",
 57 |         "# Set your authentication token\n",
 58 |         "# Replace \"your_ngrok_auth_token\" with your actual token\n",
 59 |         "ngrok.set_auth_token(\"your_ngrok_auth_token\")"
 60 |       ]
 61 |     },
 62 |     {
 63 |       "cell_type": "markdown",
 64 |       "metadata": {},
 65 |       "source": [
 66 |         "## 3. Launch Application and Generate Public URL\n",
 67 |         "\n",
 68 |         "Now we'll start the Streamlit server and create an ngrok tunnel to make it accessible online:"
 69 |       ]
 70 |     },
 71 |     {
 72 |       "cell_type": "code",
 73 |       "execution_count": null,
 74 |       "metadata": {
 75 |         "colab": {
 76 |           "base_uri": "https://localhost:8080/"
 77 |         },
 78 |         "collapsed": true,
 79 |         "id": "oahsIOXmwjl9",
 80 |         "outputId": "ee23a96c-af21-4207-deb7-9fab69e0c05e"
 81 |       },
 82 |       "outputs": [],
 83 |       "source": [
 84 |         "import subprocess\n",
 85 |         "import time\n",
 86 |         "\n",
 87 |         "print(\"🚀 Starting MoneyPrinterTurbo...\")\n",
 88 |         "# Start Streamlit server on port 8501\n",
 89 |         "streamlit_proc = subprocess.Popen([\n",
 90 |         "    \"streamlit\", \"run\", \"./webui/Main.py\", \"--server.port=8501\"\n",
 91 |         "])\n",
 92 |         "\n",
 93 |         "# Wait for the server to initialize\n",
 94 |         "time.sleep(5)\n",
 95 |         "\n",
 96 |         "print(\"🌐 Creating ngrok tunnel to expose the MoneyPrinterTurbo...\")\n",
 97 |         "public_url = ngrok.connect(8501, bind_tls=True)\n",
 98 |         "\n",
 99 |         "print(\"✅ Deployment complete! Access your MoneyPrinterTurbo at:\")\n",
100 |         "print(public_url)"
101 |       ]
102 |     }
103 |   ],
104 |   "metadata": {
105 |     "colab": {
106 |       "provenance": []
107 |     },
108 |     "kernelspec": {
109 |       "display_name": "Python 3",
110 |       "name": "python3"
111 |     },
112 |     "language_info": {
113 |       "name": "python"
114 |     }
115 |   },
116 |   "nbformat": 4,
117 |   "nbformat_minor": 0
118 | }
119 | 


--------------------------------------------------------------------------------
/docs/api.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/harry0703/MoneyPrinterTurbo/6cb5f2348714b08fd7cf04a416c13faeab1d356b/docs/api.jpg


--------------------------------------------------------------------------------
/docs/picwish.com.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/harry0703/MoneyPrinterTurbo/6cb5f2348714b08fd7cf04a416c13faeab1d356b/docs/picwish.com.jpg


--------------------------------------------------------------------------------
/docs/picwish.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/harry0703/MoneyPrinterTurbo/6cb5f2348714b08fd7cf04a416c13faeab1d356b/docs/picwish.jpg


--------------------------------------------------------------------------------
/docs/reccloud.cn.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/harry0703/MoneyPrinterTurbo/6cb5f2348714b08fd7cf04a416c13faeab1d356b/docs/reccloud.cn.jpg


--------------------------------------------------------------------------------
/docs/reccloud.com.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/harry0703/MoneyPrinterTurbo/6cb5f2348714b08fd7cf04a416c13faeab1d356b/docs/reccloud.com.jpg


--------------------------------------------------------------------------------
/docs/voice-list.txt:
--------------------------------------------------------------------------------
  1 | Name: af-ZA-AdriNeural
  2 | Gender: Female
  3 | 
  4 | Name: af-ZA-WillemNeural
  5 | Gender: Male
  6 | 
  7 | Name: am-ET-AmehaNeural
  8 | Gender: Male
  9 | 
 10 | Name: am-ET-MekdesNeural
 11 | Gender: Female
 12 | 
 13 | Name: ar-AE-FatimaNeural
 14 | Gender: Female
 15 | 
 16 | Name: ar-AE-HamdanNeural
 17 | Gender: Male
 18 | 
 19 | Name: ar-BH-AliNeural
 20 | Gender: Male
 21 | 
 22 | Name: ar-BH-LailaNeural
 23 | Gender: Female
 24 | 
 25 | Name: ar-DZ-AminaNeural
 26 | Gender: Female
 27 | 
 28 | Name: ar-DZ-IsmaelNeural
 29 | Gender: Male
 30 | 
 31 | Name: ar-EG-SalmaNeural
 32 | Gender: Female
 33 | 
 34 | Name: ar-EG-ShakirNeural
 35 | Gender: Male
 36 | 
 37 | Name: ar-IQ-BasselNeural
 38 | Gender: Male
 39 | 
 40 | Name: ar-IQ-RanaNeural
 41 | Gender: Female
 42 | 
 43 | Name: ar-JO-SanaNeural
 44 | Gender: Female
 45 | 
 46 | Name: ar-JO-TaimNeural
 47 | Gender: Male
 48 | 
 49 | Name: ar-KW-FahedNeural
 50 | Gender: Male
 51 | 
 52 | Name: ar-KW-NouraNeural
 53 | Gender: Female
 54 | 
 55 | Name: ar-LB-LaylaNeural
 56 | Gender: Female
 57 | 
 58 | Name: ar-LB-RamiNeural
 59 | Gender: Male
 60 | 
 61 | Name: ar-LY-ImanNeural
 62 | Gender: Female
 63 | 
 64 | Name: ar-LY-OmarNeural
 65 | Gender: Male
 66 | 
 67 | Name: ar-MA-JamalNeural
 68 | Gender: Male
 69 | 
 70 | Name: ar-MA-MounaNeural
 71 | Gender: Female
 72 | 
 73 | Name: ar-OM-AbdullahNeural
 74 | Gender: Male
 75 | 
 76 | Name: ar-OM-AyshaNeural
 77 | Gender: Female
 78 | 
 79 | Name: ar-QA-AmalNeural
 80 | Gender: Female
 81 | 
 82 | Name: ar-QA-MoazNeural
 83 | Gender: Male
 84 | 
 85 | Name: ar-SA-HamedNeural
 86 | Gender: Male
 87 | 
 88 | Name: ar-SA-ZariyahNeural
 89 | Gender: Female
 90 | 
 91 | Name: ar-SY-AmanyNeural
 92 | Gender: Female
 93 | 
 94 | Name: ar-SY-LaithNeural
 95 | Gender: Male
 96 | 
 97 | Name: ar-TN-HediNeural
 98 | Gender: Male
 99 | 
100 | Name: ar-TN-ReemNeural
101 | Gender: Female
102 | 
103 | Name: ar-YE-MaryamNeural
104 | Gender: Female
105 | 
106 | Name: ar-YE-SalehNeural
107 | Gender: Male
108 | 
109 | Name: az-AZ-BabekNeural
110 | Gender: Male
111 | 
112 | Name: az-AZ-BanuNeural
113 | Gender: Female
114 | 
115 | Name: bg-BG-BorislavNeural
116 | Gender: Male
117 | 
118 | Name: bg-BG-KalinaNeural
119 | Gender: Female
120 | 
121 | Name: bn-BD-NabanitaNeural
122 | Gender: Female
123 | 
124 | Name: bn-BD-PradeepNeural
125 | Gender: Male
126 | 
127 | Name: bn-IN-BashkarNeural
128 | Gender: Male
129 | 
130 | Name: bn-IN-TanishaaNeural
131 | Gender: Female
132 | 
133 | Name: bs-BA-GoranNeural
134 | Gender: Male
135 | 
136 | Name: bs-BA-VesnaNeural
137 | Gender: Female
138 | 
139 | Name: ca-ES-EnricNeural
140 | Gender: Male
141 | 
142 | Name: ca-ES-JoanaNeural
143 | Gender: Female
144 | 
145 | Name: cs-CZ-AntoninNeural
146 | Gender: Male
147 | 
148 | Name: cs-CZ-VlastaNeural
149 | Gender: Female
150 | 
151 | Name: cy-GB-AledNeural
152 | Gender: Male
153 | 
154 | Name: cy-GB-NiaNeural
155 | Gender: Female
156 | 
157 | Name: da-DK-ChristelNeural
158 | Gender: Female
159 | 
160 | Name: da-DK-JeppeNeural
161 | Gender: Male
162 | 
163 | Name: de-AT-IngridNeural
164 | Gender: Female
165 | 
166 | Name: de-AT-JonasNeural
167 | Gender: Male
168 | 
169 | Name: de-CH-JanNeural
170 | Gender: Male
171 | 
172 | Name: de-CH-LeniNeural
173 | Gender: Female
174 | 
175 | Name: de-DE-AmalaNeural
176 | Gender: Female
177 | 
178 | Name: de-DE-ConradNeural
179 | Gender: Male
180 | 
181 | Name: de-DE-FlorianMultilingualNeural
182 | Gender: Male
183 | 
184 | Name: de-DE-KatjaNeural
185 | Gender: Female
186 | 
187 | Name: de-DE-KillianNeural
188 | Gender: Male
189 | 
190 | Name: de-DE-SeraphinaMultilingualNeural
191 | Gender: Female
192 | 
193 | Name: el-GR-AthinaNeural
194 | Gender: Female
195 | 
196 | Name: el-GR-NestorasNeural
197 | Gender: Male
198 | 
199 | Name: en-AU-NatashaNeural
200 | Gender: Female
201 | 
202 | Name: en-AU-WilliamNeural
203 | Gender: Male
204 | 
205 | Name: en-CA-ClaraNeural
206 | Gender: Female
207 | 
208 | Name: en-CA-LiamNeural
209 | Gender: Male
210 | 
211 | Name: en-GB-LibbyNeural
212 | Gender: Female
213 | 
214 | Name: en-GB-MaisieNeural
215 | Gender: Female
216 | 
217 | Name: en-GB-RyanNeural
218 | Gender: Male
219 | 
220 | Name: en-GB-SoniaNeural
221 | Gender: Female
222 | 
223 | Name: en-GB-ThomasNeural
224 | Gender: Male
225 | 
226 | Name: en-HK-SamNeural
227 | Gender: Male
228 | 
229 | Name: en-HK-YanNeural
230 | Gender: Female
231 | 
232 | Name: en-IE-ConnorNeural
233 | Gender: Male
234 | 
235 | Name: en-IE-EmilyNeural
236 | Gender: Female
237 | 
238 | Name: en-IN-NeerjaExpressiveNeural
239 | Gender: Female
240 | 
241 | Name: en-IN-NeerjaNeural
242 | Gender: Female
243 | 
244 | Name: en-IN-PrabhatNeural
245 | Gender: Male
246 | 
247 | Name: en-KE-AsiliaNeural
248 | Gender: Female
249 | 
250 | Name: en-KE-ChilembaNeural
251 | Gender: Male
252 | 
253 | Name: en-NG-AbeoNeural
254 | Gender: Male
255 | 
256 | Name: en-NG-EzinneNeural
257 | Gender: Female
258 | 
259 | Name: en-NZ-MitchellNeural
260 | Gender: Male
261 | 
262 | Name: en-NZ-MollyNeural
263 | Gender: Female
264 | 
265 | Name: en-PH-JamesNeural
266 | Gender: Male
267 | 
268 | Name: en-PH-RosaNeural
269 | Gender: Female
270 | 
271 | Name: en-SG-LunaNeural
272 | Gender: Female
273 | 
274 | Name: en-SG-WayneNeural
275 | Gender: Male
276 | 
277 | Name: en-TZ-ElimuNeural
278 | Gender: Male
279 | 
280 | Name: en-TZ-ImaniNeural
281 | Gender: Female
282 | 
283 | Name: en-US-AnaNeural
284 | Gender: Female
285 | 
286 | Name: en-US-AndrewNeural
287 | Gender: Male
288 | 
289 | Name: en-US-AriaNeural
290 | Gender: Female
291 | 
292 | Name: en-US-AvaNeural
293 | Gender: Female
294 | 
295 | Name: en-US-BrianNeural
296 | Gender: Male
297 | 
298 | Name: en-US-ChristopherNeural
299 | Gender: Male
300 | 
301 | Name: en-US-EmmaNeural
302 | Gender: Female
303 | 
304 | Name: en-US-EricNeural
305 | Gender: Male
306 | 
307 | Name: en-US-GuyNeural
308 | Gender: Male
309 | 
310 | Name: en-US-JennyNeural
311 | Gender: Female
312 | 
313 | Name: en-US-MichelleNeural
314 | Gender: Female
315 | 
316 | Name: en-US-RogerNeural
317 | Gender: Male
318 | 
319 | Name: en-US-SteffanNeural
320 | Gender: Male
321 | 
322 | Name: en-ZA-LeahNeural
323 | Gender: Female
324 | 
325 | Name: en-ZA-LukeNeural
326 | Gender: Male
327 | 
328 | Name: es-AR-ElenaNeural
329 | Gender: Female
330 | 
331 | Name: es-AR-TomasNeural
332 | Gender: Male
333 | 
334 | Name: es-BO-MarceloNeural
335 | Gender: Male
336 | 
337 | Name: es-BO-SofiaNeural
338 | Gender: Female
339 | 
340 | Name: es-CL-CatalinaNeural
341 | Gender: Female
342 | 
343 | Name: es-CL-LorenzoNeural
344 | Gender: Male
345 | 
346 | Name: es-CO-GonzaloNeural
347 | Gender: Male
348 | 
349 | Name: es-CO-SalomeNeural
350 | Gender: Female
351 | 
352 | Name: es-CR-JuanNeural
353 | Gender: Male
354 | 
355 | Name: es-CR-MariaNeural
356 | Gender: Female
357 | 
358 | Name: es-CU-BelkysNeural
359 | Gender: Female
360 | 
361 | Name: es-CU-ManuelNeural
362 | Gender: Male
363 | 
364 | Name: es-DO-EmilioNeural
365 | Gender: Male
366 | 
367 | Name: es-DO-RamonaNeural
368 | Gender: Female
369 | 
370 | Name: es-EC-AndreaNeural
371 | Gender: Female
372 | 
373 | Name: es-EC-LuisNeural
374 | Gender: Male
375 | 
376 | Name: es-ES-AlvaroNeural
377 | Gender: Male
378 | 
379 | Name: es-ES-ElviraNeural
380 | Gender: Female
381 | 
382 | Name: es-ES-XimenaNeural
383 | Gender: Female
384 | 
385 | Name: es-GQ-JavierNeural
386 | Gender: Male
387 | 
388 | Name: es-GQ-TeresaNeural
389 | Gender: Female
390 | 
391 | Name: es-GT-AndresNeural
392 | Gender: Male
393 | 
394 | Name: es-GT-MartaNeural
395 | Gender: Female
396 | 
397 | Name: es-HN-CarlosNeural
398 | Gender: Male
399 | 
400 | Name: es-HN-KarlaNeural
401 | Gender: Female
402 | 
403 | Name: es-MX-DaliaNeural
404 | Gender: Female
405 | 
406 | Name: es-MX-JorgeNeural
407 | Gender: Male
408 | 
409 | Name: es-NI-FedericoNeural
410 | Gender: Male
411 | 
412 | Name: es-NI-YolandaNeural
413 | Gender: Female
414 | 
415 | Name: es-PA-MargaritaNeural
416 | Gender: Female
417 | 
418 | Name: es-PA-RobertoNeural
419 | Gender: Male
420 | 
421 | Name: es-PE-AlexNeural
422 | Gender: Male
423 | 
424 | Name: es-PE-CamilaNeural
425 | Gender: Female
426 | 
427 | Name: es-PR-KarinaNeural
428 | Gender: Female
429 | 
430 | Name: es-PR-VictorNeural
431 | Gender: Male
432 | 
433 | Name: es-PY-MarioNeural
434 | Gender: Male
435 | 
436 | Name: es-PY-TaniaNeural
437 | Gender: Female
438 | 
439 | Name: es-SV-LorenaNeural
440 | Gender: Female
441 | 
442 | Name: es-SV-RodrigoNeural
443 | Gender: Male
444 | 
445 | Name: es-US-AlonsoNeural
446 | Gender: Male
447 | 
448 | Name: es-US-PalomaNeural
449 | Gender: Female
450 | 
451 | Name: es-UY-MateoNeural
452 | Gender: Male
453 | 
454 | Name: es-UY-ValentinaNeural
455 | Gender: Female
456 | 
457 | Name: es-VE-PaolaNeural
458 | Gender: Female
459 | 
460 | Name: es-VE-SebastianNeural
461 | Gender: Male
462 | 
463 | Name: et-EE-AnuNeural
464 | Gender: Female
465 | 
466 | Name: et-EE-KertNeural
467 | Gender: Male
468 | 
469 | Name: fa-IR-DilaraNeural
470 | Gender: Female
471 | 
472 | Name: fa-IR-FaridNeural
473 | Gender: Male
474 | 
475 | Name: fi-FI-HarriNeural
476 | Gender: Male
477 | 
478 | Name: fi-FI-NooraNeural
479 | Gender: Female
480 | 
481 | Name: fil-PH-AngeloNeural
482 | Gender: Male
483 | 
484 | Name: fil-PH-BlessicaNeural
485 | Gender: Female
486 | 
487 | Name: fr-BE-CharlineNeural
488 | Gender: Female
489 | 
490 | Name: fr-BE-GerardNeural
491 | Gender: Male
492 | 
493 | Name: fr-CA-AntoineNeural
494 | Gender: Male
495 | 
496 | Name: fr-CA-JeanNeural
497 | Gender: Male
498 | 
499 | Name: fr-CA-SylvieNeural
500 | Gender: Female
501 | 
502 | Name: fr-CA-ThierryNeural
503 | Gender: Male
504 | 
505 | Name: fr-CH-ArianeNeural
506 | Gender: Female
507 | 
508 | Name: fr-CH-FabriceNeural
509 | Gender: Male
510 | 
511 | Name: fr-FR-DeniseNeural
512 | Gender: Female
513 | 
514 | Name: fr-FR-EloiseNeural
515 | Gender: Female
516 | 
517 | Name: fr-FR-HenriNeural
518 | Gender: Male
519 | 
520 | Name: fr-FR-RemyMultilingualNeural
521 | Gender: Male
522 | 
523 | Name: fr-FR-VivienneMultilingualNeural
524 | Gender: Female
525 | 
526 | Name: ga-IE-ColmNeural
527 | Gender: Male
528 | 
529 | Name: ga-IE-OrlaNeural
530 | Gender: Female
531 | 
532 | Name: gl-ES-RoiNeural
533 | Gender: Male
534 | 
535 | Name: gl-ES-SabelaNeural
536 | Gender: Female
537 | 
538 | Name: gu-IN-DhwaniNeural
539 | Gender: Female
540 | 
541 | Name: gu-IN-NiranjanNeural
542 | Gender: Male
543 | 
544 | Name: he-IL-AvriNeural
545 | Gender: Male
546 | 
547 | Name: he-IL-HilaNeural
548 | Gender: Female
549 | 
550 | Name: hi-IN-MadhurNeural
551 | Gender: Male
552 | 
553 | Name: hi-IN-SwaraNeural
554 | Gender: Female
555 | 
556 | Name: hr-HR-GabrijelaNeural
557 | Gender: Female
558 | 
559 | Name: hr-HR-SreckoNeural
560 | Gender: Male
561 | 
562 | Name: hu-HU-NoemiNeural
563 | Gender: Female
564 | 
565 | Name: hu-HU-TamasNeural
566 | Gender: Male
567 | 
568 | Name: id-ID-ArdiNeural
569 | Gender: Male
570 | 
571 | Name: id-ID-GadisNeural
572 | Gender: Female
573 | 
574 | Name: is-IS-GudrunNeural
575 | Gender: Female
576 | 
577 | Name: is-IS-GunnarNeural
578 | Gender: Male
579 | 
580 | Name: it-IT-DiegoNeural
581 | Gender: Male
582 | 
583 | Name: it-IT-ElsaNeural
584 | Gender: Female
585 | 
586 | Name: it-IT-GiuseppeNeural
587 | Gender: Male
588 | 
589 | Name: it-IT-IsabellaNeural
590 | Gender: Female
591 | 
592 | Name: ja-JP-KeitaNeural
593 | Gender: Male
594 | 
595 | Name: ja-JP-NanamiNeural
596 | Gender: Female
597 | 
598 | Name: jv-ID-DimasNeural
599 | Gender: Male
600 | 
601 | Name: jv-ID-SitiNeural
602 | Gender: Female
603 | 
604 | Name: ka-GE-EkaNeural
605 | Gender: Female
606 | 
607 | Name: ka-GE-GiorgiNeural
608 | Gender: Male
609 | 
610 | Name: kk-KZ-AigulNeural
611 | Gender: Female
612 | 
613 | Name: kk-KZ-DauletNeural
614 | Gender: Male
615 | 
616 | Name: km-KH-PisethNeural
617 | Gender: Male
618 | 
619 | Name: km-KH-SreymomNeural
620 | Gender: Female
621 | 
622 | Name: kn-IN-GaganNeural
623 | Gender: Male
624 | 
625 | Name: kn-IN-SapnaNeural
626 | Gender: Female
627 | 
628 | Name: ko-KR-HyunsuNeural
629 | Gender: Male
630 | 
631 | Name: ko-KR-InJoonNeural
632 | Gender: Male
633 | 
634 | Name: ko-KR-SunHiNeural
635 | Gender: Female
636 | 
637 | Name: lo-LA-ChanthavongNeural
638 | Gender: Male
639 | 
640 | Name: lo-LA-KeomanyNeural
641 | Gender: Female
642 | 
643 | Name: lt-LT-LeonasNeural
644 | Gender: Male
645 | 
646 | Name: lt-LT-OnaNeural
647 | Gender: Female
648 | 
649 | Name: lv-LV-EveritaNeural
650 | Gender: Female
651 | 
652 | Name: lv-LV-NilsNeural
653 | Gender: Male
654 | 
655 | Name: mk-MK-AleksandarNeural
656 | Gender: Male
657 | 
658 | Name: mk-MK-MarijaNeural
659 | Gender: Female
660 | 
661 | Name: ml-IN-MidhunNeural
662 | Gender: Male
663 | 
664 | Name: ml-IN-SobhanaNeural
665 | Gender: Female
666 | 
667 | Name: mn-MN-BataaNeural
668 | Gender: Male
669 | 
670 | Name: mn-MN-YesuiNeural
671 | Gender: Female
672 | 
673 | Name: mr-IN-AarohiNeural
674 | Gender: Female
675 | 
676 | Name: mr-IN-ManoharNeural
677 | Gender: Male
678 | 
679 | Name: ms-MY-OsmanNeural
680 | Gender: Male
681 | 
682 | Name: ms-MY-YasminNeural
683 | Gender: Female
684 | 
685 | Name: mt-MT-GraceNeural
686 | Gender: Female
687 | 
688 | Name: mt-MT-JosephNeural
689 | Gender: Male
690 | 
691 | Name: my-MM-NilarNeural
692 | Gender: Female
693 | 
694 | Name: my-MM-ThihaNeural
695 | Gender: Male
696 | 
697 | Name: nb-NO-FinnNeural
698 | Gender: Male
699 | 
700 | Name: nb-NO-PernilleNeural
701 | Gender: Female
702 | 
703 | Name: ne-NP-HemkalaNeural
704 | Gender: Female
705 | 
706 | Name: ne-NP-SagarNeural
707 | Gender: Male
708 | 
709 | Name: nl-BE-ArnaudNeural
710 | Gender: Male
711 | 
712 | Name: nl-BE-DenaNeural
713 | Gender: Female
714 | 
715 | Name: nl-NL-ColetteNeural
716 | Gender: Female
717 | 
718 | Name: nl-NL-FennaNeural
719 | Gender: Female
720 | 
721 | Name: nl-NL-MaartenNeural
722 | Gender: Male
723 | 
724 | Name: pl-PL-MarekNeural
725 | Gender: Male
726 | 
727 | Name: pl-PL-ZofiaNeural
728 | Gender: Female
729 | 
730 | Name: ps-AF-GulNawazNeural
731 | Gender: Male
732 | 
733 | Name: ps-AF-LatifaNeural
734 | Gender: Female
735 | 
736 | Name: pt-BR-AntonioNeural
737 | Gender: Male
738 | 
739 | Name: pt-BR-FranciscaNeural
740 | Gender: Female
741 | 
742 | Name: pt-BR-ThalitaNeural
743 | Gender: Female
744 | 
745 | Name: pt-PT-DuarteNeural
746 | Gender: Male
747 | 
748 | Name: pt-PT-RaquelNeural
749 | Gender: Female
750 | 
751 | Name: ro-RO-AlinaNeural
752 | Gender: Female
753 | 
754 | Name: ro-RO-EmilNeural
755 | Gender: Male
756 | 
757 | Name: ru-RU-DmitryNeural
758 | Gender: Male
759 | 
760 | Name: ru-RU-SvetlanaNeural
761 | Gender: Female
762 | 
763 | Name: si-LK-SameeraNeural
764 | Gender: Male
765 | 
766 | Name: si-LK-ThiliniNeural
767 | Gender: Female
768 | 
769 | Name: sk-SK-LukasNeural
770 | Gender: Male
771 | 
772 | Name: sk-SK-ViktoriaNeural
773 | Gender: Female
774 | 
775 | Name: sl-SI-PetraNeural
776 | Gender: Female
777 | 
778 | Name: sl-SI-RokNeural
779 | Gender: Male
780 | 
781 | Name: so-SO-MuuseNeural
782 | Gender: Male
783 | 
784 | Name: so-SO-UbaxNeural
785 | Gender: Female
786 | 
787 | Name: sq-AL-AnilaNeural
788 | Gender: Female
789 | 
790 | Name: sq-AL-IlirNeural
791 | Gender: Male
792 | 
793 | Name: sr-RS-NicholasNeural
794 | Gender: Male
795 | 
796 | Name: sr-RS-SophieNeural
797 | Gender: Female
798 | 
799 | Name: su-ID-JajangNeural
800 | Gender: Male
801 | 
802 | Name: su-ID-TutiNeural
803 | Gender: Female
804 | 
805 | Name: sv-SE-MattiasNeural
806 | Gender: Male
807 | 
808 | Name: sv-SE-SofieNeural
809 | Gender: Female
810 | 
811 | Name: sw-KE-RafikiNeural
812 | Gender: Male
813 | 
814 | Name: sw-KE-ZuriNeural
815 | Gender: Female
816 | 
817 | Name: sw-TZ-DaudiNeural
818 | Gender: Male
819 | 
820 | Name: sw-TZ-RehemaNeural
821 | Gender: Female
822 | 
823 | Name: ta-IN-PallaviNeural
824 | Gender: Female
825 | 
826 | Name: ta-IN-ValluvarNeural
827 | Gender: Male
828 | 
829 | Name: ta-LK-KumarNeural
830 | Gender: Male
831 | 
832 | Name: ta-LK-SaranyaNeural
833 | Gender: Female
834 | 
835 | Name: ta-MY-KaniNeural
836 | Gender: Female
837 | 
838 | Name: ta-MY-SuryaNeural
839 | Gender: Male
840 | 
841 | Name: ta-SG-AnbuNeural
842 | Gender: Male
843 | 
844 | Name: ta-SG-VenbaNeural
845 | Gender: Female
846 | 
847 | Name: te-IN-MohanNeural
848 | Gender: Male
849 | 
850 | Name: te-IN-ShrutiNeural
851 | Gender: Female
852 | 
853 | Name: th-TH-NiwatNeural
854 | Gender: Male
855 | 
856 | Name: th-TH-PremwadeeNeural
857 | Gender: Female
858 | 
859 | Name: tr-TR-AhmetNeural
860 | Gender: Male
861 | 
862 | Name: tr-TR-EmelNeural
863 | Gender: Female
864 | 
865 | Name: uk-UA-OstapNeural
866 | Gender: Male
867 | 
868 | Name: uk-UA-PolinaNeural
869 | Gender: Female
870 | 
871 | Name: ur-IN-GulNeural
872 | Gender: Female
873 | 
874 | Name: ur-IN-SalmanNeural
875 | Gender: Male
876 | 
877 | Name: ur-PK-AsadNeural
878 | Gender: Male
879 | 
880 | Name: ur-PK-UzmaNeural
881 | Gender: Female
882 | 
883 | Name: uz-UZ-MadinaNeural
884 | Gender: Female
885 | 
886 | Name: uz-UZ-SardorNeural
887 | Gender: Male
888 | 
889 | Name: vi-VN-HoaiMyNeural
890 | Gender: Female
891 | 
892 | Name: vi-VN-NamMinhNeural
893 | Gender: Male
894 | 
895 | Name: zh-CN-XiaoxiaoNeural
896 | Gender: Female
897 | 
898 | Name: zh-CN-XiaoyiNeural
899 | Gender: Female
900 | 
901 | Name: zh-CN-YunjianNeural
902 | Gender: Male
903 | 
904 | Name: zh-CN-YunxiNeural
905 | Gender: Male
906 | 
907 | Name: zh-CN-YunxiaNeural
908 | Gender: Male
909 | 
910 | Name: zh-CN-YunyangNeural
911 | Gender: Male
912 | 
913 | Name: zh-CN-liaoning-XiaobeiNeural
914 | Gender: Female
915 | 
916 | Name: zh-CN-shaanxi-XiaoniNeural
917 | Gender: Female
918 | 
919 | Name: zh-HK-HiuGaaiNeural
920 | Gender: Female
921 | 
922 | Name: zh-HK-HiuMaanNeural
923 | Gender: Female
924 | 
925 | Name: zh-HK-WanLungNeural
926 | Gender: Male
927 | 
928 | Name: zh-TW-HsiaoChenNeural
929 | Gender: Female
930 | 
931 | Name: zh-TW-HsiaoYuNeural
932 | Gender: Female
933 | 
934 | Name: zh-TW-YunJheNeural
935 | Gender: Male
936 | 
937 | Name: zu-ZA-ThandoNeural
938 | Gender: Female
939 | 
940 | Name: zu-ZA-ThembaNeural
941 | Gender: Male
942 | 


--------------------------------------------------------------------------------
/docs/webui-en.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/harry0703/MoneyPrinterTurbo/6cb5f2348714b08fd7cf04a416c13faeab1d356b/docs/webui-en.jpg


--------------------------------------------------------------------------------
/docs/webui.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/harry0703/MoneyPrinterTurbo/6cb5f2348714b08fd7cf04a416c13faeab1d356b/docs/webui.jpg


--------------------------------------------------------------------------------
/main.py:
--------------------------------------------------------------------------------
 1 | import uvicorn
 2 | from loguru import logger
 3 | 
 4 | from app.config import config
 5 | 
 6 | if __name__ == "__main__":
 7 |     logger.info(
 8 |         "start server, docs: http://127.0.0.1:" + str(config.listen_port) + "/docs"
 9 |     )
10 |     uvicorn.run(
11 |         app="app.asgi:app",
12 |         host=config.listen_host,
13 |         port=config.listen_port,
14 |         reload=config.reload_debug,
15 |         log_level="warning",
16 |     )
17 | 


--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
 1 | moviepy==2.1.2
 2 | streamlit==1.45.0
 3 | edge_tts==6.1.19
 4 | fastapi==0.115.6
 5 | uvicorn==0.32.1
 6 | openai==1.56.1
 7 | faster-whisper==1.1.0
 8 | loguru==0.7.3
 9 | google.generativeai==0.8.3
10 | dashscope==1.20.14
11 | g4f==0.5.2.2
12 | azure-cognitiveservices-speech==1.41.1
13 | redis==5.2.0
14 | python-multipart==0.0.19
15 | pyyaml
16 | requests>=2.31.0
17 | 


--------------------------------------------------------------------------------
/resource/fonts/Charm-Bold.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/harry0703/MoneyPrinterTurbo/6cb5f2348714b08fd7cf04a416c13faeab1d356b/resource/fonts/Charm-Bold.ttf


--------------------------------------------------------------------------------
/resource/fonts/Charm-Regular.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/harry0703/MoneyPrinterTurbo/6cb5f2348714b08fd7cf04a416c13faeab1d356b/resource/fonts/Charm-Regular.ttf


--------------------------------------------------------------------------------
/resource/fonts/MicrosoftYaHeiBold.ttc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/harry0703/MoneyPrinterTurbo/6cb5f2348714b08fd7cf04a416c13faeab1d356b/resource/fonts/MicrosoftYaHeiBold.ttc


--------------------------------------------------------------------------------
/resource/fonts/MicrosoftYaHeiNormal.ttc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/harry0703/MoneyPrinterTurbo/6cb5f2348714b08fd7cf04a416c13faeab1d356b/resource/fonts/MicrosoftYaHeiNormal.ttc


--------------------------------------------------------------------------------
/resource/fonts/STHeitiLight.ttc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/harry0703/MoneyPrinterTurbo/6cb5f2348714b08fd7cf04a416c13faeab1d356b/resource/fonts/STHeitiLight.ttc


--------------------------------------------------------------------------------
/resource/fonts/STHeitiMedium.ttc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/harry0703/MoneyPrinterTurbo/6cb5f2348714b08fd7cf04a416c13faeab1d356b/resource/fonts/STHeitiMedium.ttc


--------------------------------------------------------------------------------
/resource/fonts/UTM Kabel KT.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/harry0703/MoneyPrinterTurbo/6cb5f2348714b08fd7cf04a416c13faeab1d356b/resource/fonts/UTM Kabel KT.ttf


--------------------------------------------------------------------------------
/resource/public/index.html:
--------------------------------------------------------------------------------
 1 | <!DOCTYPE html>
 2 | <html lang="en">
 3 | <head>
 4 |     <meta charset="UTF-8">
 5 |     <title>MoneyPrinterTurbo</title>
 6 | </head>
 7 | <body>
 8 | <h1>MoneyPrinterTurbo</h1>
 9 | <a href="https://github.com/harry0703/MoneyPrinterTurbo">https://github.com/harry0703/MoneyPrinterTurbo</a>
10 | <p>
11 |     只需提供一个视频 主题 或 关键词 ,就可以全自动生成视频文案、视频素材、视频字幕、视频背景音乐,然后合成一个高清的短视频。
12 | </p>
13 | 
14 | <p>
15 |     Simply provide a topic or keyword for a video, and it will automatically generate the video copy, video materials,
16 |     video subtitles, and video background music before synthesizing a high-definition short video.
17 | </p>
18 | </body>
19 | </html>


--------------------------------------------------------------------------------
/resource/songs/output000.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/harry0703/MoneyPrinterTurbo/6cb5f2348714b08fd7cf04a416c13faeab1d356b/resource/songs/output000.mp3


--------------------------------------------------------------------------------
/resource/songs/output001.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/harry0703/MoneyPrinterTurbo/6cb5f2348714b08fd7cf04a416c13faeab1d356b/resource/songs/output001.mp3


--------------------------------------------------------------------------------
/resource/songs/output002.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/harry0703/MoneyPrinterTurbo/6cb5f2348714b08fd7cf04a416c13faeab1d356b/resource/songs/output002.mp3


--------------------------------------------------------------------------------
/resource/songs/output003.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/harry0703/MoneyPrinterTurbo/6cb5f2348714b08fd7cf04a416c13faeab1d356b/resource/songs/output003.mp3


--------------------------------------------------------------------------------
/resource/songs/output004.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/harry0703/MoneyPrinterTurbo/6cb5f2348714b08fd7cf04a416c13faeab1d356b/resource/songs/output004.mp3


--------------------------------------------------------------------------------
/resource/songs/output005.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/harry0703/MoneyPrinterTurbo/6cb5f2348714b08fd7cf04a416c13faeab1d356b/resource/songs/output005.mp3


--------------------------------------------------------------------------------
/resource/songs/output006.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/harry0703/MoneyPrinterTurbo/6cb5f2348714b08fd7cf04a416c13faeab1d356b/resource/songs/output006.mp3


--------------------------------------------------------------------------------
/resource/songs/output007.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/harry0703/MoneyPrinterTurbo/6cb5f2348714b08fd7cf04a416c13faeab1d356b/resource/songs/output007.mp3


--------------------------------------------------------------------------------
/resource/songs/output008.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/harry0703/MoneyPrinterTurbo/6cb5f2348714b08fd7cf04a416c13faeab1d356b/resource/songs/output008.mp3


--------------------------------------------------------------------------------
/resource/songs/output009.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/harry0703/MoneyPrinterTurbo/6cb5f2348714b08fd7cf04a416c13faeab1d356b/resource/songs/output009.mp3


--------------------------------------------------------------------------------
/resource/songs/output010.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/harry0703/MoneyPrinterTurbo/6cb5f2348714b08fd7cf04a416c13faeab1d356b/resource/songs/output010.mp3


--------------------------------------------------------------------------------
/resource/songs/output011.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/harry0703/MoneyPrinterTurbo/6cb5f2348714b08fd7cf04a416c13faeab1d356b/resource/songs/output011.mp3


--------------------------------------------------------------------------------
/resource/songs/output012.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/harry0703/MoneyPrinterTurbo/6cb5f2348714b08fd7cf04a416c13faeab1d356b/resource/songs/output012.mp3


--------------------------------------------------------------------------------
/resource/songs/output013.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/harry0703/MoneyPrinterTurbo/6cb5f2348714b08fd7cf04a416c13faeab1d356b/resource/songs/output013.mp3


--------------------------------------------------------------------------------
/resource/songs/output014.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/harry0703/MoneyPrinterTurbo/6cb5f2348714b08fd7cf04a416c13faeab1d356b/resource/songs/output014.mp3


--------------------------------------------------------------------------------
/resource/songs/output015.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/harry0703/MoneyPrinterTurbo/6cb5f2348714b08fd7cf04a416c13faeab1d356b/resource/songs/output015.mp3


--------------------------------------------------------------------------------
/resource/songs/output016.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/harry0703/MoneyPrinterTurbo/6cb5f2348714b08fd7cf04a416c13faeab1d356b/resource/songs/output016.mp3


--------------------------------------------------------------------------------
/resource/songs/output017.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/harry0703/MoneyPrinterTurbo/6cb5f2348714b08fd7cf04a416c13faeab1d356b/resource/songs/output017.mp3


--------------------------------------------------------------------------------
/resource/songs/output018.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/harry0703/MoneyPrinterTurbo/6cb5f2348714b08fd7cf04a416c13faeab1d356b/resource/songs/output018.mp3


--------------------------------------------------------------------------------
/resource/songs/output019.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/harry0703/MoneyPrinterTurbo/6cb5f2348714b08fd7cf04a416c13faeab1d356b/resource/songs/output019.mp3


--------------------------------------------------------------------------------
/resource/songs/output020.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/harry0703/MoneyPrinterTurbo/6cb5f2348714b08fd7cf04a416c13faeab1d356b/resource/songs/output020.mp3


--------------------------------------------------------------------------------
/resource/songs/output021.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/harry0703/MoneyPrinterTurbo/6cb5f2348714b08fd7cf04a416c13faeab1d356b/resource/songs/output021.mp3


--------------------------------------------------------------------------------
/resource/songs/output022.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/harry0703/MoneyPrinterTurbo/6cb5f2348714b08fd7cf04a416c13faeab1d356b/resource/songs/output022.mp3


--------------------------------------------------------------------------------
/resource/songs/output023.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/harry0703/MoneyPrinterTurbo/6cb5f2348714b08fd7cf04a416c13faeab1d356b/resource/songs/output023.mp3


--------------------------------------------------------------------------------
/resource/songs/output024.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/harry0703/MoneyPrinterTurbo/6cb5f2348714b08fd7cf04a416c13faeab1d356b/resource/songs/output024.mp3


--------------------------------------------------------------------------------
/resource/songs/output025.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/harry0703/MoneyPrinterTurbo/6cb5f2348714b08fd7cf04a416c13faeab1d356b/resource/songs/output025.mp3


--------------------------------------------------------------------------------
/resource/songs/output027.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/harry0703/MoneyPrinterTurbo/6cb5f2348714b08fd7cf04a416c13faeab1d356b/resource/songs/output027.mp3


--------------------------------------------------------------------------------
/resource/songs/output028.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/harry0703/MoneyPrinterTurbo/6cb5f2348714b08fd7cf04a416c13faeab1d356b/resource/songs/output028.mp3


--------------------------------------------------------------------------------
/resource/songs/output029.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/harry0703/MoneyPrinterTurbo/6cb5f2348714b08fd7cf04a416c13faeab1d356b/resource/songs/output029.mp3


--------------------------------------------------------------------------------
/test/README.md:
--------------------------------------------------------------------------------
 1 | # MoneyPrinterTurbo Test Directory
 2 | 
 3 | This directory contains unit tests for the **MoneyPrinterTurbo** project.
 4 | 
 5 | ## Directory Structure
 6 | 
 7 | - `services/`: Tests for components in the `app/services` directory  
 8 |   - `test_video.py`: Tests for the video service  
 9 |   - `test_task.py`: Tests for the task service  
10 |   - `test_voice.py`: Tests for the voice service  
11 | 
12 | ## Running Tests
13 | 
14 | You can run the tests using Python’s built-in `unittest` framework:
15 | 
16 | ```bash
17 | # Run all tests
18 | python -m unittest discover -s test
19 | 
20 | # Run a specific test file
21 | python -m unittest test/services/test_video.py
22 | 
23 | # Run a specific test class
24 | python -m unittest test.services.test_video.TestVideoService
25 | 
26 | # Run a specific test method
27 | python -m unittest test.services.test_video.TestVideoService.test_preprocess_video
28 | ````
29 | 
30 | ## Adding New Tests
31 | 
32 | To add tests for other components, follow these guidelines:
33 | 
34 | 1. Create test files prefixed with `test_` in the appropriate subdirectory
35 | 2. Use `unittest.TestCase` as the base class for your test classes
36 | 3. Name test methods with the `test_` prefix
37 | 
38 | ## Test Resources
39 | 
40 | Place any resource files required for testing in the `test/resources` directory.


--------------------------------------------------------------------------------
/test/__init__.py:
--------------------------------------------------------------------------------
1 | # Unit test package for test
2 | 


--------------------------------------------------------------------------------
/test/resources/1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/harry0703/MoneyPrinterTurbo/6cb5f2348714b08fd7cf04a416c13faeab1d356b/test/resources/1.png


--------------------------------------------------------------------------------
/test/resources/1.png.mp4:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/harry0703/MoneyPrinterTurbo/6cb5f2348714b08fd7cf04a416c13faeab1d356b/test/resources/1.png.mp4


--------------------------------------------------------------------------------
/test/resources/2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/harry0703/MoneyPrinterTurbo/6cb5f2348714b08fd7cf04a416c13faeab1d356b/test/resources/2.png


--------------------------------------------------------------------------------
/test/resources/2.png.mp4:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/harry0703/MoneyPrinterTurbo/6cb5f2348714b08fd7cf04a416c13faeab1d356b/test/resources/2.png.mp4


--------------------------------------------------------------------------------
/test/resources/3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/harry0703/MoneyPrinterTurbo/6cb5f2348714b08fd7cf04a416c13faeab1d356b/test/resources/3.png


--------------------------------------------------------------------------------
/test/resources/3.png.mp4:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/harry0703/MoneyPrinterTurbo/6cb5f2348714b08fd7cf04a416c13faeab1d356b/test/resources/3.png.mp4


--------------------------------------------------------------------------------
/test/resources/4.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/harry0703/MoneyPrinterTurbo/6cb5f2348714b08fd7cf04a416c13faeab1d356b/test/resources/4.png


--------------------------------------------------------------------------------
/test/resources/5.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/harry0703/MoneyPrinterTurbo/6cb5f2348714b08fd7cf04a416c13faeab1d356b/test/resources/5.png


--------------------------------------------------------------------------------
/test/resources/6.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/harry0703/MoneyPrinterTurbo/6cb5f2348714b08fd7cf04a416c13faeab1d356b/test/resources/6.png


--------------------------------------------------------------------------------
/test/resources/7.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/harry0703/MoneyPrinterTurbo/6cb5f2348714b08fd7cf04a416c13faeab1d356b/test/resources/7.png


--------------------------------------------------------------------------------
/test/resources/8.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/harry0703/MoneyPrinterTurbo/6cb5f2348714b08fd7cf04a416c13faeab1d356b/test/resources/8.png


--------------------------------------------------------------------------------
/test/resources/9.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/harry0703/MoneyPrinterTurbo/6cb5f2348714b08fd7cf04a416c13faeab1d356b/test/resources/9.png


--------------------------------------------------------------------------------
/test/services/__init__.py:
--------------------------------------------------------------------------------
1 | # Unit test package for services


--------------------------------------------------------------------------------
/test/services/test_task.py:
--------------------------------------------------------------------------------
 1 | import unittest
 2 | import os
 3 | import sys
 4 | from pathlib import Path
 5 | 
 6 | # add project root to python path
 7 | sys.path.insert(0, str(Path(__file__).parent.parent.parent))
 8 | 
 9 | from app.services import task as tm
10 | from app.models.schema import MaterialInfo, VideoParams
11 | 
12 | resources_dir = os.path.join(os.path.dirname(os.path.dirname(__file__)), "resources")
13 | 
14 | class TestTaskService(unittest.TestCase):
15 |     def setUp(self):
16 |         pass
17 |     
18 |     def tearDown(self):
19 |         pass
20 |     
21 |     def test_task_local_materials(self):
22 |         task_id = "00000000-0000-0000-0000-000000000000"
23 |         video_materials=[]
24 |         for i in range(1, 4):
25 |             video_materials.append(MaterialInfo(
26 |                 provider="local",
27 |                 url=os.path.join(resources_dir, f"{i}.png"),
28 |                 duration=0
29 |             ))
30 | 
31 |         params = VideoParams(
32 |             video_subject="金钱的作用",
33 |             video_script="金钱不仅是交换媒介,更是社会资源的分配工具。它能满足基本生存需求,如食物和住房,也能提供教育、医疗等提升生活品质的机会。拥有足够的金钱意味着更多选择权,比如职业自由或创业可能。但金钱的作用也有边界,它无法直接购买幸福、健康或真诚的人际关系。过度追逐财富可能导致价值观扭曲,忽视精神层面的需求。理想的状态是理性看待金钱,将其作为实现目标的工具而非终极目的。",
34 |             video_terms="money importance, wealth and society, financial freedom, money and happiness, role of money",
35 |             video_aspect="9:16",
36 |             video_concat_mode="random",
37 |             video_transition_mode="None",
38 |             video_clip_duration=3,
39 |             video_count=1,
40 |             video_source="local",
41 |             video_materials=video_materials,
42 |             video_language="",
43 |             voice_name="zh-CN-XiaoxiaoNeural-Female",
44 |             voice_volume=1.0,
45 |             voice_rate=1.0,
46 |             bgm_type="random",
47 |             bgm_file="",
48 |             bgm_volume=0.2,
49 |             subtitle_enabled=True,
50 |             subtitle_position="bottom",
51 |             custom_position=70.0,
52 |             font_name="MicrosoftYaHeiBold.ttc",
53 |             text_fore_color="#FFFFFF",
54 |             text_background_color=True,
55 |             font_size=60,
56 |             stroke_color="#000000",
57 |             stroke_width=1.5,
58 |             n_threads=2,
59 |             paragraph_number=1
60 |         )
61 |         result = tm.start(task_id=task_id, params=params)
62 |         print(result)
63 |     
64 | 
65 | if __name__ == "__main__":
66 |     unittest.main() 


--------------------------------------------------------------------------------
/test/services/test_video.py:
--------------------------------------------------------------------------------
 1 | 
 2 | import unittest
 3 | import os
 4 | import sys
 5 | from pathlib import Path
 6 | from moviepy import (
 7 |     VideoFileClip,
 8 | )
 9 | # add project root to python path
10 | sys.path.insert(0, str(Path(__file__).parent.parent.parent))
11 | from app.models.schema import MaterialInfo
12 | from app.services import video as vd
13 | from app.utils import utils
14 | 
15 | resources_dir = os.path.join(os.path.dirname(os.path.dirname(__file__)), "resources")
16 | 
17 | class TestVideoService(unittest.TestCase):
18 |     def setUp(self):
19 |         self.test_img_path = os.path.join(resources_dir, "1.png")
20 |     
21 |     def tearDown(self):
22 |         pass
23 |     
24 |     def test_preprocess_video(self):
25 |         if not os.path.exists(self.test_img_path):
26 |             self.fail(f"test image not found: {self.test_img_path}")
27 |         
28 |         # test preprocess_video function
29 |         m = MaterialInfo()
30 |         m.url = self.test_img_path
31 |         m.provider = "local"
32 |         print(m)
33 |         
34 |         materials = vd.preprocess_video([m], clip_duration=4)
35 |         print(materials)
36 |         
37 |         # verify result
38 |         self.assertIsNotNone(materials)
39 |         self.assertEqual(len(materials), 1)
40 |         self.assertTrue(materials[0].url.endswith(".mp4"))
41 |         
42 |         # moviepy get video info
43 |         clip = VideoFileClip(materials[0].url)
44 |         print(clip)
45 |         
46 |         # clean generated test video file
47 |         if os.path.exists(materials[0].url):
48 |             os.remove(materials[0].url)
49 |     
50 |     def test_wrap_text(self):
51 |         """test text wrapping function"""
52 |         try:
53 |             font_path = os.path.join(utils.font_dir(), "STHeitiMedium.ttc")
54 |             if not os.path.exists(font_path):
55 |                 self.fail(f"font file not found: {font_path}")
56 |                 
57 |             # test english text wrapping
58 |             test_text_en = "This is a test text for wrapping long sentences in english language"
59 |             
60 |             wrapped_text_en, text_height_en = vd.wrap_text(
61 |                 text=test_text_en,
62 |                 max_width=300,
63 |                 font=font_path,
64 |                 fontsize=30
65 |             )
66 |             print(wrapped_text_en, text_height_en)
67 |             # verify text is wrapped
68 |             self.assertIn("\n", wrapped_text_en)
69 |             
70 |             # test chinese text wrapping
71 |             test_text_zh = "这是一段用来测试中文长句换行的文本内容,应该会根据宽度限制进行换行处理"
72 |             wrapped_text_zh, text_height_zh = vd.wrap_text(
73 |                 text=test_text_zh,
74 |                 max_width=300,
75 |                 font=font_path,
76 |                 fontsize=30
77 |             )   
78 |             print(wrapped_text_zh, text_height_zh)
79 |             # verify chinese text is wrapped
80 |             self.assertIn("\n", wrapped_text_zh)
81 |         except Exception as e:
82 |             self.fail(f"test wrap_text failed: {str(e)}")
83 | 
84 | if __name__ == "__main__":
85 |     unittest.main() 


--------------------------------------------------------------------------------
/test/services/test_voice.py:
--------------------------------------------------------------------------------
  1 | import asyncio
  2 | import unittest
  3 | import os
  4 | import sys
  5 | from pathlib import Path
  6 | 
  7 | # add project root to python path
  8 | sys.path.insert(0, str(Path(__file__).parent.parent.parent))
  9 | 
 10 | from app.utils import utils
 11 | from app.services import voice as vs
 12 | 
 13 | temp_dir = utils.storage_dir("temp")
 14 | 
 15 | text_en = """
 16 | What is the meaning of life? 
 17 | This question has puzzled philosophers, scientists, and thinkers of all kinds for centuries. 
 18 | Throughout history, various cultures and individuals have come up with their interpretations and beliefs around the purpose of life. 
 19 | Some say it's to seek happiness and self-fulfillment, while others believe it's about contributing to the welfare of others and making a positive impact in the world. 
 20 | Despite the myriad of perspectives, one thing remains clear: the meaning of life is a deeply personal concept that varies from one person to another. 
 21 | It's an existential inquiry that encourages us to reflect on our values, desires, and the essence of our existence.
 22 | """
 23 | 
 24 | text_zh = """
 25 | 预计未来3天深圳冷空气活动频繁,未来两天持续阴天有小雨,出门带好雨具;
 26 | 10-11日持续阴天有小雨,日温差小,气温在13-17℃之间,体感阴凉;
 27 | 12日天气短暂好转,早晚清凉;
 28 | """
 29 | 
 30 | voice_rate=1.0
 31 | voice_volume=1.0
 32 |                     
 33 | class TestVoiceService(unittest.TestCase):
 34 |     def setUp(self):
 35 |         self.loop = asyncio.new_event_loop()
 36 |         asyncio.set_event_loop(self.loop)
 37 |     
 38 |     def tearDown(self):
 39 |         self.loop.close()
 40 |     
 41 |     def test_siliconflow(self):
 42 |         voice_name = "siliconflow:FunAudioLLM/CosyVoice2-0.5B:alex-Male"
 43 |         voice_name = vs.parse_voice_name(voice_name)
 44 |         
 45 |         async def _do():
 46 |             parts = voice_name.split(":")
 47 |             if len(parts) >= 3:
 48 |                 model = parts[1]
 49 |                 # 移除性别后缀,例如 "alex-Male" -> "alex"
 50 |                 voice_with_gender = parts[2]
 51 |                 voice = voice_with_gender.split("-")[0]
 52 |                 # 构建完整的voice参数,格式为 "model:voice"
 53 |                 full_voice = f"{model}:{voice}"
 54 |                 voice_file = f"{temp_dir}/tts-siliconflow-{voice}.mp3"
 55 |                 subtitle_file = f"{temp_dir}/tts-siliconflow-{voice}.srt"
 56 |                 sub_maker = vs.siliconflow_tts(
 57 |                     text=text_zh, model=model, voice=full_voice, voice_file=voice_file, voice_rate=voice_rate, voice_volume=voice_volume
 58 |                 )
 59 |                 if not sub_maker:
 60 |                     self.fail("siliconflow tts failed")
 61 |                 vs.create_subtitle(sub_maker=sub_maker, text=text_zh, subtitle_file=subtitle_file)
 62 |                 audio_duration = vs.get_audio_duration(sub_maker)
 63 |                 print(f"voice: {voice_name}, audio duration: {audio_duration}s")
 64 |             else:
 65 |                 self.fail("siliconflow invalid voice name")
 66 | 
 67 |         self.loop.run_until_complete(_do())
 68 |     
 69 |     def test_azure_tts_v1(self):
 70 |         voice_name = "zh-CN-XiaoyiNeural-Female"
 71 |         voice_name = vs.parse_voice_name(voice_name)
 72 |         print(voice_name)
 73 |         
 74 |         voice_file = f"{temp_dir}/tts-azure-v1-{voice_name}.mp3"
 75 |         subtitle_file = f"{temp_dir}/tts-azure-v1-{voice_name}.srt"
 76 |         sub_maker = vs.azure_tts_v1(
 77 |             text=text_zh, voice_name=voice_name, voice_file=voice_file, voice_rate=voice_rate
 78 |         )
 79 |         if not sub_maker:
 80 |             self.fail("azure tts v1 failed")
 81 |         vs.create_subtitle(sub_maker=sub_maker, text=text_zh, subtitle_file=subtitle_file)
 82 |         audio_duration = vs.get_audio_duration(sub_maker)
 83 |         print(f"voice: {voice_name}, audio duration: {audio_duration}s")
 84 | 
 85 |     def test_azure_tts_v2(self):
 86 |         voice_name = "zh-CN-XiaoxiaoMultilingualNeural-V2-Female"
 87 |         voice_name = vs.parse_voice_name(voice_name)
 88 |         print(voice_name)
 89 | 
 90 |         async def _do():
 91 |             voice_file = f"{temp_dir}/tts-azure-v2-{voice_name}.mp3"
 92 |             subtitle_file = f"{temp_dir}/tts-azure-v2-{voice_name}.srt"
 93 |             sub_maker = vs.azure_tts_v2(
 94 |                 text=text_zh, voice_name=voice_name, voice_file=voice_file
 95 |             )
 96 |             if not sub_maker:
 97 |                 self.fail("azure tts v2 failed")
 98 |             vs.create_subtitle(sub_maker=sub_maker, text=text_zh, subtitle_file=subtitle_file)
 99 |             audio_duration = vs.get_audio_duration(sub_maker)
100 |             print(f"voice: {voice_name}, audio duration: {audio_duration}s")
101 | 
102 |         self.loop.run_until_complete(_do())
103 | 
104 | if __name__ == "__main__":
105 |     # python -m unittest test.services.test_voice.TestVoiceService.test_azure_tts_v1
106 |     # python -m unittest test.services.test_voice.TestVoiceService.test_azure_tts_v2
107 |     unittest.main() 


--------------------------------------------------------------------------------
/webui.bat:
--------------------------------------------------------------------------------
1 | @echo off
2 | set CURRENT_DIR=%CD%
3 | echo ***** Current directory: %CURRENT_DIR% *****
4 | set PYTHONPATH=%CURRENT_DIR%
5 | 
6 | rem set HF_ENDPOINT=https://hf-mirror.com
7 | streamlit run .\webui\Main.py --browser.gatherUsageStats=False --server.enableCORS=True


--------------------------------------------------------------------------------
/webui.sh:
--------------------------------------------------------------------------------
1 | # If you could not download the model from the official site, you can use the mirror site.
2 | # Just remove the comment of the following line .
3 | # 如果你无法从官方网站下载模型,你可以使用镜像网站。
4 | # 只需要移除下面一行的注释即可。
5 | 
6 | # export HF_ENDPOINT=https://hf-mirror.com
7 | 
8 | streamlit run ./webui/Main.py --browser.serverAddress="0.0.0.0" --server.enableCORS=True --browser.gatherUsageStats=False


--------------------------------------------------------------------------------
/webui/.streamlit/config.toml:
--------------------------------------------------------------------------------
1 | [browser]
2 | gatherUsageStats = false


--------------------------------------------------------------------------------
/webui/i18n/de.json:
--------------------------------------------------------------------------------
  1 | {
  2 |   "Language": "Deutsch",
  3 |   "Translation": {
  4 |     "Login Required": "Anmeldung erforderlich",
  5 |     "Please login to access settings": "Bitte melden Sie sich an, um auf die Einstellungen zuzugreifen",
  6 |     "Username": "Benutzername",
  7 |     "Password": "Passwort",
  8 |     "Login": "Anmelden",
  9 |     "Login Error": "Anmeldefehler",
 10 |     "Incorrect username or password": "Falscher Benutzername oder Passwort",
 11 |     "Please enter your username and password": "Bitte geben Sie Ihren Benutzernamen und Ihr Passwort ein",
 12 |     "Video Script Settings": "**Drehbuch / Topic des Videos**",
 13 |     "Video Subject": "Worum soll es in dem Video gehen? (Geben Sie ein Keyword an, :red[Dank KI wird automatisch ein Drehbuch generieren])",
 14 |     "Script Language": "Welche Sprache soll zum Generieren von Drehbüchern  verwendet werden? :red[KI generiert anhand dieses Begriffs das Drehbuch]",
 15 |     "Generate Video Script and Keywords": "Klicken Sie hier, um mithilfe von KI ein [Video Drehbuch] und [Video Keywords] basierend auf dem **Keyword** zu generieren.",
 16 |     "Auto Detect": "Automatisch erkennen",
 17 |     "Video Script": "Drehbuch (Storybook) (:blue[① Optional, KI generiert  ② Die richtige Zeichensetzung hilft bei der Erstellung von Untertiteln])",
 18 |     "Generate Video Keywords": "Klicken Sie, um KI zum Generieren zu verwenden [Video Keywords] basierend auf dem **Drehbuch**",
 19 |     "Please Enter the Video Subject": "Bitte geben Sie zuerst das Drehbuch an",
 20 |     "Generating Video Script and Keywords": "KI generiert ein Drehbuch und Schlüsselwörter...",
 21 |     "Generating Video Keywords": "KI generiert Video-Schlüsselwörter...",
 22 |     "Video Keywords": "Video Schlüsselwörter (:blue[① Optional, KI generiert ② Verwende **, (Kommas)** zur Trennung der Wörter, in englischer Sprache])",
 23 |     "Video Settings": "**Video Einstellungen**",
 24 |     "Video Concat Mode": "Videoverkettungsmodus",
 25 |     "Random": "Zufällige Verkettung (empfohlen)",
 26 |     "Sequential": "Sequentielle Verkettung",
 27 |     "Video Transition Mode": "Video Übergangsmodus",
 28 |     "None": "Kein Übergang",
 29 |     "Shuffle": "Zufällige Übergänge",
 30 |     "FadeIn": "FadeIn",
 31 |     "FadeOut": "FadeOut",
 32 |     "SlideIn": "SlideIn",
 33 |     "SlideOut": "SlideOut",
 34 |     "Video Ratio": "Video-Seitenverhältnis",
 35 |     "Portrait": "Portrait 9:16",
 36 |     "Landscape": "Landschaft 16:9",
 37 |     "Clip Duration": "Maximale Dauer einzelner Videoclips in sekunden",
 38 |     "Number of Videos Generated Simultaneously": "Anzahl der parallel generierten Videos",
 39 |     "Audio Settings": "**Audio Einstellungen**",
 40 |     "Speech Synthesis": "Sprachausgabe",
 41 |     "Speech Region": "Region(:red[Erforderlich,[Region abrufen](https://portal.azure.com/#view/Microsoft_Azure_ProjectOxford/CognitiveServicesHub/~/SpeechServices)])",
 42 |     "Speech Key": "API-Schlüssel(:red[Erforderlich,[API-Schlüssel abrufen](https://portal.azure.com/#view/Microsoft_Azure_ProjectOxford/CognitiveServicesHub/~/SpeechServices)])",
 43 |     "Speech Volume": "Lautstärke der Sprachausgabe",
 44 |     "Speech Rate": "Lesegeschwindigkeit (1,0 bedeutet 1x)",
 45 |     "Male": "Männlich",
 46 |     "Female": "Weiblich",
 47 |     "Background Music": "Hintergrundmusik",
 48 |     "No Background Music": "Ohne Hintergrundmusik",
 49 |     "Random Background Music": "Zufällig erzeugte Hintergrundmusik",
 50 |     "Custom Background Music": "Benutzerdefinierte Hintergrundmusik",
 51 |     "Custom Background Music File": "Bitte gib den Pfad zur Musikdatei an:",
 52 |     "Background Music Volume": "Lautstärke: (0.2 entspricht 20%, sollte nicht zu laut sein)",
 53 |     "Subtitle Settings": "**Untertitel-Einstellungen**",
 54 |     "Enable Subtitles": "Untertitel aktivieren (Wenn diese Option deaktiviert ist, werden die Einstellungen nicht genutzt)",
 55 |     "Font": "Schriftart des Untertitels",
 56 |     "Position": "Ausrichtung des Untertitels",
 57 |     "Top": "Oben",
 58 |     "Center": "Mittig",
 59 |     "Bottom": "Unten (empfohlen)",
 60 |     "Custom": "Benutzerdefinierte Position (70, was 70% von oben bedeutet)",
 61 |     "Font Size": "Schriftgröße für Untertitel",
 62 |     "Font Color": "Schriftfarbe",
 63 |     "Stroke Color": "Kontur",
 64 |     "Stroke Width": "Breite der Untertitelkontur",
 65 |     "Generate Video": "Generiere Videos durch KI",
 66 |     "Video Script and Subject Cannot Both Be Empty": "Das Video-Thema und Drehbuch dürfen nicht beide leer sein",
 67 |     "Generating Video": "Video wird erstellt, bitte warten...",
 68 |     "Start Generating Video": "Beginne mit der Generierung",
 69 |     "Video Generation Completed": "Video erfolgreich generiert",
 70 |     "Video Generation Failed": "Video Generierung fehlgeschlagen",
 71 |     "You can download the generated video from the following links": "Sie können das generierte Video über die folgenden Links herunterladen",
 72 |     "Basic Settings": "**Grundeinstellungen** (:blue[Klicken zum Erweitern])",
 73 |     "Language": "Sprache",
 74 |     "Pexels API Key": "Pexels API-Schlüssel ([API-Schlüssel abrufen](https://www.pexels.com/api/))",
 75 |     "Pixabay API Key": "Pixabay API-Schlüssel ([API-Schlüssel abrufen](https://pixabay.com/api/docs/#api_search_videos))",
 76 |     "LLM Provider": "KI-Modellanbieter",
 77 |     "API Key": "API-Schlüssel (:red[Erforderlich])",
 78 |     "Base Url": "Basis-URL",
 79 |     "Account ID": "Konto-ID (Aus dem Cloudflare-Dashboard)",
 80 |     "Model Name": "Modellname",
 81 |     "Please Enter the LLM API Key": "Bitte geben Sie den **KI-Modell API-Schlüssel** ein",
 82 |     "Please Enter the Pexels API Key": "Bitte geben Sie den **Pexels API-Schlüssel** ein",
 83 |     "Please Enter the Pixabay API Key": "Bitte geben Sie den **Pixabay API-Schlüssel** ein",
 84 |     "Get Help": "Wenn Sie Hilfe benötigen oder Fragen haben, können Sie dem Discord beitreten: https://harryai.cc",
 85 |     "Video Source": "Videoquelle",
 86 |     "TikTok": "TikTok (TikTok-Unterstützung kommt bald)",
 87 |     "Bilibili": "Bilibili (Bilibili-Unterstützung kommt bald)",
 88 |     "Xiaohongshu": "Xiaohongshu (Xiaohongshu-Unterstützung kommt bald)",
 89 |     "Local file": "Lokale Datei",
 90 |     "Play Voice": "Sprachausgabe abspielen",
 91 |     "Voice Example": "Dies ist ein Beispieltext zum Testen der Sprachsynthese",
 92 |     "Synthesizing Voice": "Sprachsynthese läuft, bitte warten...",
 93 |     "TTS Provider": "Sprachsynthese-Anbieter auswählen",
 94 |     "TTS Servers": "TTS-Server",
 95 |     "No voices available for the selected TTS server. Please select another server.": "Keine Stimmen für den ausgewählten TTS-Server verfügbar. Bitte wählen Sie einen anderen Server.",
 96 |     "SiliconFlow API Key": "SiliconFlow API-Schlüssel",
 97 |     "SiliconFlow TTS Settings": "SiliconFlow TTS-Einstellungen",
 98 |     "Speed: Range [0.25, 4.0], default is 1.0": "Geschwindigkeit: Bereich [0.25, 4.0], Standardwert ist 1.0",
 99 |     "Volume: Uses Speech Volume setting, default 1.0 maps to gain 0": "Lautstärke: Verwendet die Sprachlautstärke-Einstellung, Standardwert 1.0 entspricht Verstärkung 0",
100 |     "Hide Log": "Protokoll ausblenden",
101 |     "Hide Basic Settings": "Basis-Einstellungen ausblenden\n\nWenn diese Option deaktiviert ist, wird die Basis-Einstellungen-Leiste nicht auf der Seite angezeigt.\n\nWenn Sie sie erneut anzeigen möchten, setzen Sie `hide_config = false` in `config.toml`",
102 |     "LLM Settings": "**LLM-Einstellungen**",
103 |     "Video Source Settings": "**Videoquellen-Einstellungen**"
104 |   }
105 | }


--------------------------------------------------------------------------------
/webui/i18n/en.json:
--------------------------------------------------------------------------------
  1 | {
  2 |   "Language": "English",
  3 |   "Translation": {
  4 |     "Login Required": "Login Required",
  5 |     "Please login to access settings": "Please login to access settings",
  6 |     "Username": "Username",
  7 |     "Password": "Password",
  8 |     "Login": "Login",
  9 |     "Login Error": "Login Error",
 10 |     "Incorrect username or password": "Incorrect username or password",
 11 |     "Please enter your username and password": "Please enter your username and password",
 12 |     "Video Script Settings": "**Video Script Settings**",
 13 |     "Video Subject": "Video Subject (Provide a keyword, :red[AI will automatically generate] video script)",
 14 |     "Script Language": "Language for Generating Video Script (AI will automatically output based on the language of your subject)",
 15 |     "Generate Video Script and Keywords": "Click to use AI to generate [Video Script] and [Video Keywords] based on **subject**",
 16 |     "Auto Detect": "Auto Detect",
 17 |     "Video Script": "Video Script (:blue[① Optional, AI generated  ② Proper punctuation helps with subtitle generation])",
 18 |     "Generate Video Keywords": "Click to use AI to generate [Video Keywords] based on **script**",
 19 |     "Please Enter the Video Subject": "Please Enter the Video Script First",
 20 |     "Generating Video Script and Keywords": "AI is generating video script and keywords...",
 21 |     "Generating Video Keywords": "AI is generating video keywords...",
 22 |     "Video Keywords": "Video Keywords (:blue[① Optional, AI generated ② Use **English commas** for separation, English only])",
 23 |     "Video Settings": "**Video Settings**",
 24 |     "Video Concat Mode": "Video Concatenation Mode",
 25 |     "Random": "Random Concatenation (Recommended)",
 26 |     "Sequential": "Sequential Concatenation",
 27 |     "Video Transition Mode": "Video Transition Mode",
 28 |     "None": "None",
 29 |     "Shuffle": "Shuffle",
 30 |     "FadeIn": "FadeIn",
 31 |     "FadeOut": "FadeOut",
 32 |     "SlideIn": "SlideIn",
 33 |     "SlideOut": "SlideOut",
 34 |     "Video Ratio": "Video Aspect Ratio",
 35 |     "Portrait": "Portrait 9:16",
 36 |     "Landscape": "Landscape 16:9",
 37 |     "Clip Duration": "Maximum Duration of Video Clips (seconds)",
 38 |     "Number of Videos Generated Simultaneously": "Number of Videos Generated Simultaneously",
 39 |     "Audio Settings": "**Audio Settings**",
 40 |     "Speech Synthesis": "Speech Synthesis Voice",
 41 |     "Speech Region": "Region(:red[Required,[Get Region](https://portal.azure.com/#view/Microsoft_Azure_ProjectOxford/CognitiveServicesHub/~/SpeechServices)])",
 42 |     "Speech Key": "API Key(:red[Required,[Get API Key](https://portal.azure.com/#view/Microsoft_Azure_ProjectOxford/CognitiveServicesHub/~/SpeechServices)])",
 43 |     "Speech Volume": "Speech Volume (1.0 represents 100%)",
 44 |     "Speech Rate": "Speech Rate (1.0 means 1x speed)",
 45 |     "Male": "Male",
 46 |     "Female": "Female",
 47 |     "Background Music": "Background Music",
 48 |     "No Background Music": "No Background Music",
 49 |     "Random Background Music": "Random Background Music",
 50 |     "Custom Background Music": "Custom Background Music",
 51 |     "Custom Background Music File": "Please enter the file path for custom background music:",
 52 |     "Background Music Volume": "Background Music Volume (0.2 represents 20%, background music should not be too loud)",
 53 |     "Subtitle Settings": "**Subtitle Settings**",
 54 |     "Enable Subtitles": "Enable Subtitles (If unchecked, the settings below will not take effect)",
 55 |     "Font": "Subtitle Font",
 56 |     "Position": "Subtitle Position",
 57 |     "Top": "Top",
 58 |     "Center": "Center",
 59 |     "Bottom": "Bottom (Recommended)",
 60 |     "Custom": "Custom position (70, indicating 70% down from the top)",
 61 |     "Font Size": "Subtitle Font Size",
 62 |     "Font Color": "Subtitle Font Color",
 63 |     "Stroke Color": "Subtitle Outline Color",
 64 |     "Stroke Width": "Subtitle Outline Width",
 65 |     "Generate Video": "Generate Video",
 66 |     "Video Script and Subject Cannot Both Be Empty": "Video Subject and Video Script cannot both be empty",
 67 |     "Generating Video": "Generating video, please wait...",
 68 |     "Start Generating Video": "Start Generating Video",
 69 |     "Video Generation Completed": "Video Generation Completed",
 70 |     "Video Generation Failed": "Video Generation Failed",
 71 |     "You can download the generated video from the following links": "You can download the generated video from the following links",
 72 |     "Pexels API Key": "Pexels API Key ([Get API Key](https://www.pexels.com/api/))",
 73 |     "Pixabay API Key": "Pixabay API Key ([Get API Key](https://pixabay.com/api/docs/#api_search_videos))",
 74 |     "Basic Settings": "**Basic Settings** (:blue[Click to expand])",
 75 |     "Language": "Language",
 76 |     "LLM Provider": "LLM Provider",
 77 |     "API Key": "API Key (:red[Required])",
 78 |     "Base Url": "Base Url",
 79 |     "Account ID": "Account ID (Get from Cloudflare dashboard)",
 80 |     "Model Name": "Model Name",
 81 |     "Please Enter the LLM API Key": "Please Enter the **LLM API Key**",
 82 |     "Please Enter the Pexels API Key": "Please Enter the **Pexels API Key**",
 83 |     "Please Enter the Pixabay API Key": "Please Enter the **Pixabay API Key**",
 84 |     "Get Help": "If you need help, or have any questions, you can join discord for help: https://harryai.cc",
 85 |     "Video Source": "Video Source",
 86 |     "TikTok": "TikTok (TikTok support is coming soon)",
 87 |     "Bilibili": "Bilibili (Bilibili support is coming soon)",
 88 |     "Xiaohongshu": "Xiaohongshu (Xiaohongshu support is coming soon)",
 89 |     "Local file": "Local file",
 90 |     "Play Voice": "Play Voice",
 91 |     "Voice Example": "This is an example text for testing speech synthesis",
 92 |     "Synthesizing Voice": "Synthesizing voice, please wait...",
 93 |     "TTS Provider": "Select the voice synthesis provider",
 94 |     "TTS Servers": "TTS Servers",
 95 |     "No voices available for the selected TTS server. Please select another server.": "No voices available for the selected TTS server. Please select another server.",
 96 |     "SiliconFlow API Key": "SiliconFlow API Key [Click to get](https://cloud.siliconflow.cn/account/ak)",
 97 |     "SiliconFlow TTS Settings": "SiliconFlow TTS Settings",
 98 |     "Speed: Range [0.25, 4.0], default is 1.0": "Speed: Range [0.25, 4.0], default is 1.0",
 99 |     "Volume: Uses Speech Volume setting, default 1.0 maps to gain 0": "Volume: Uses Speech Volume setting, default 1.0 maps to gain 0",
100 |     "Hide Log": "Hide Log",
101 |     "Hide Basic Settings": "Hide Basic Settings\n\nHidden, the basic settings panel will not be displayed on the page.\n\nIf you need to display it again, please set `hide_config = false` in `config.toml`",
102 |     "LLM Settings": "**LLM Settings**",
103 |     "Video Source Settings": "**Video Source Settings**"
104 |   }
105 | }


--------------------------------------------------------------------------------
/webui/i18n/pt.json:
--------------------------------------------------------------------------------
  1 | {
  2 |   "Language": "Português Brasileiro",
  3 |   "Translation": {
  4 |     "Login Required": "Login Necessário",
  5 |     "Please login to access settings": "Por favor, faça login para acessar as configurações",
  6 |     "Username": "Nome de usuário",
  7 |     "Password": "Senha",
  8 |     "Login": "Entrar",
  9 |     "Login Error": "Erro de Login",
 10 |     "Incorrect username or password": "Nome de usuário ou senha incorretos",
 11 |     "Please enter your username and password": "Por favor, digite seu nome de usuário e senha",
 12 |     "Video Script Settings": "**Configurações do Roteiro do Vídeo**",
 13 |     "Video Subject": "Tema do Vídeo (Forneça uma palavra-chave, :red[a IA irá gerar automaticamente] o roteiro do vídeo)",
 14 |     "Script Language": "Idioma para Gerar o Roteiro do Vídeo (a IA irá gerar automaticamente com base no idioma do seu tema)",
 15 |     "Generate Video Script and Keywords": "Clique para usar a IA para gerar o [Roteiro do Vídeo] e as [Palavras-chave do Vídeo] com base no **tema**",
 16 |     "Auto Detect": "Detectar Automaticamente",
 17 |     "Video Script": "Roteiro do Vídeo (:blue[① Opcional, gerado pela IA  ② Pontuação adequada ajuda na geração de legendas])",
 18 |     "Generate Video Keywords": "Clique para usar a IA para gerar [Palavras-chave do Vídeo] com base no **roteiro**",
 19 |     "Please Enter the Video Subject": "Por favor, insira o Roteiro do Vídeo primeiro",
 20 |     "Generating Video Script and Keywords": "A IA está gerando o roteiro do vídeo e as palavras-chave...",
 21 |     "Generating Video Keywords": "A IA está gerando as palavras-chave do vídeo...",
 22 |     "Video Keywords": "Palavras-chave do Vídeo (:blue[① Opcional, gerado pela IA ② Use **vírgulas em inglês** para separar, somente em inglês])",
 23 |     "Video Settings": "**Configurações do Vídeo**",
 24 |     "Video Concat Mode": "Modo de Concatenação de Vídeo",
 25 |     "Random": "Concatenação Aleatória (Recomendado)",
 26 |     "Sequential": "Concatenação Sequencial",
 27 |     "Video Transition Mode": "Modo de Transição de Vídeo",
 28 |     "None": "Nenhuma Transição",
 29 |     "Shuffle": "Transição Aleatória",
 30 |     "FadeIn": "FadeIn",
 31 |     "FadeOut": "FadeOut",
 32 |     "SlideIn": "SlideIn",
 33 |     "SlideOut": "SlideOut",
 34 |     "Video Ratio": "Proporção do Vídeo",
 35 |     "Portrait": "Retrato 9:16",
 36 |     "Landscape": "Paisagem 16:9",
 37 |     "Clip Duration": "Duração Máxima dos Clipes de Vídeo (segundos)",
 38 |     "Number of Videos Generated Simultaneously": "Número de Vídeos Gerados Simultaneamente",
 39 |     "Audio Settings": "**Configurações de Áudio**",
 40 |     "Speech Synthesis": "Voz de Síntese de Fala",
 41 |     "Speech Region": "Região(:red[Obrigatório,[Obter Região](https://portal.azure.com/#view/Microsoft_Azure_ProjectOxford/CognitiveServicesHub/~/SpeechServices)])",
 42 |     "Speech Key": "Chave da API(:red[Obrigatório,[Obter Chave da API](https://portal.azure.com/#view/Microsoft_Azure_ProjectOxford/CognitiveServicesHub/~/SpeechServices)])",
 43 |     "Speech Volume": "Volume da Fala (1.0 representa 100%)",
 44 |     "Speech Rate": "Velocidade da Fala (1.0 significa velocidade 1x)",
 45 |     "Male": "Masculino",
 46 |     "Female": "Feminino",
 47 |     "Background Music": "Música de Fundo",
 48 |     "No Background Music": "Sem Música de Fundo",
 49 |     "Random Background Music": "Música de Fundo Aleatória",
 50 |     "Custom Background Music": "Música de Fundo Personalizada",
 51 |     "Custom Background Music File": "Por favor, insira o caminho do arquivo para a música de fundo personalizada:",
 52 |     "Background Music Volume": "Volume da Música de Fundo (0.2 representa 20%, a música de fundo não deve ser muito alta)",
 53 |     "Subtitle Settings": "**Configurações de Legendas**",
 54 |     "Enable Subtitles": "Ativar Legendas (Se desmarcado, as configurações abaixo não terão efeito)",
 55 |     "Font": "Fonte da Legenda",
 56 |     "Position": "Posição da Legenda",
 57 |     "Top": "Superior",
 58 |     "Center": "Centralizar",
 59 |     "Bottom": "Inferior (Recomendado)",
 60 |     "Custom": "Posição personalizada (70, indicando 70% abaixo do topo)",
 61 |     "Font Size": "Tamanho da Fonte da Legenda",
 62 |     "Font Color": "Cor da Fonte da Legenda",
 63 |     "Stroke Color": "Cor do Contorno da Legenda",
 64 |     "Stroke Width": "Largura do Contorno da Legenda",
 65 |     "Generate Video": "Gerar Vídeo",
 66 |     "Video Script and Subject Cannot Both Be Empty": "O Tema do Vídeo e o Roteiro do Vídeo não podem estar ambos vazios",
 67 |     "Generating Video": "Gerando vídeo, por favor aguarde...",
 68 |     "Start Generating Video": "Começar a Gerar Vídeo",
 69 |     "Video Generation Completed": "Geração do Vídeo Concluída",
 70 |     "Video Generation Failed": "Falha na Geração do Vídeo",
 71 |     "You can download the generated video from the following links": "Você pode baixar o vídeo gerado a partir dos seguintes links",
 72 |     "Basic Settings": "**Configurações Básicas** (:blue[Clique para expandir])",
 73 |     "Language": "Idioma",
 74 |     "Pexels API Key": "Chave da API do Pexels ([Obter Chave da API](https://www.pexels.com/api/))",
 75 |     "Pixabay API Key": "Chave da API do Pixabay ([Obter Chave da API](https://pixabay.com/api/docs/#api_search_videos))",
 76 |     "LLM Provider": "Provedor LLM",
 77 |     "API Key": "Chave da API (:red[Obrigatório])",
 78 |     "Base Url": "URL Base",
 79 |     "Account ID": "ID da Conta (Obter no painel do Cloudflare)",
 80 |     "Model Name": "Nome do Modelo",
 81 |     "Please Enter the LLM API Key": "Por favor, insira a **Chave da API LLM**",
 82 |     "Please Enter the Pexels API Key": "Por favor, insira a **Chave da API do Pexels**",
 83 |     "Please Enter the Pixabay API Key": "Por favor, insira a **Chave da API do Pixabay**",
 84 |     "Get Help": "Se precisar de ajuda ou tiver alguma dúvida, você pode entrar no discord para obter ajuda: https://harryai.cc",
 85 |     "Video Source": "Fonte do Vídeo",
 86 |     "TikTok": "TikTok (Suporte para TikTok em breve)",
 87 |     "Bilibili": "Bilibili (Suporte para Bilibili em breve)",
 88 |     "Xiaohongshu": "Xiaohongshu (Suporte para Xiaohongshu em breve)",
 89 |     "Local file": "Arquivo local",
 90 |     "Play Voice": "Reproduzir Voz",
 91 |     "Voice Example": "Este é um exemplo de texto para testar a síntese de fala",
 92 |     "Synthesizing Voice": "Sintetizando voz, por favor aguarde...",
 93 |     "TTS Provider": "Selecione o provedor de síntese de voz",
 94 |     "TTS Servers": "Servidores TTS",
 95 |     "No voices available for the selected TTS server. Please select another server.": "Não há vozes disponíveis para o servidor TTS selecionado. Por favor, selecione outro servidor.",
 96 |     "SiliconFlow API Key": "Chave API do SiliconFlow",
 97 |     "SiliconFlow TTS Settings": "Configurações do SiliconFlow TTS",
 98 |     "Speed: Range [0.25, 4.0], default is 1.0": "Velocidade: Intervalo [0.25, 4.0], o padrão é 1.0",
 99 |     "Volume: Uses Speech Volume setting, default 1.0 maps to gain 0": "Volume: Usa a configuração de Volume de Fala, o padrão 1.0 corresponde ao ganho 0",
100 |     "Hide Log": "Ocultar Log",
101 |     "Hide Basic Settings": "Ocultar Configurações Básicas\n\nOculto, o painel de configurações básicas não será exibido na página.\n\nSe precisar exibi-lo novamente, defina `hide_config = false` em `config.toml`",
102 |     "LLM Settings": "**Configurações do LLM**",
103 |     "Video Source Settings": "**Configurações da Fonte do Vídeo**"
104 |   }
105 | }


--------------------------------------------------------------------------------
/webui/i18n/vi.json:
--------------------------------------------------------------------------------
  1 | {
  2 |   "Language": "Tiếng Việt",
  3 |   "Translation": {
  4 |     "Login Required": "Yêu cầu đăng nhập",
  5 |     "Please login to access settings": "Vui lòng đăng nhập để truy cập cài đặt",
  6 |     "Username": "Tên đăng nhập",
  7 |     "Password": "Mật khẩu",
  8 |     "Login": "Đăng nhập",
  9 |     "Login Error": "Lỗi đăng nhập",
 10 |     "Incorrect username or password": "Tên đăng nhập hoặc mật khẩu không chính xác",
 11 |     "Please enter your username and password": "Vui lòng nhập tên đăng nhập và mật khẩu của bạn",
 12 |     "Video Script Settings": "**Cài Đặt Kịch Bản Video**",
 13 |     "Video Subject": "Chủ Đề Video (Cung cấp một từ khóa, :red[AI sẽ tự động tạo ra] kịch bản video)",
 14 |     "Script Language": "Ngôn Ngữ cho Việc Tạo Kịch Bản Video (AI sẽ tự động xuất ra dựa trên ngôn ngữ của chủ đề của bạn)",
 15 |     "Generate Video Script and Keywords": "Nhấn để sử dụng AI để tạo [Kịch Bản Video] và [Từ Khóa Video] dựa trên **chủ đề**",
 16 |     "Auto Detect": "Tự Động Phát Hiện",
 17 |     "Video Script": "Kịch Bản Video (:blue[① Tùy chọn, AI tạo ra  ② Dấu câu chính xác giúp việc tạo phụ đề)",
 18 |     "Generate Video Keywords": "Nhấn để sử dụng AI để tạo [Từ Khóa Video] dựa trên **kịch bản**",
 19 |     "Please Enter the Video Subject": "Vui lòng Nhập Kịch Bản Video Trước",
 20 |     "Generating Video Script and Keywords": "AI đang tạo kịch bản video và từ khóa...",
 21 |     "Generating Video Keywords": "AI đang tạo từ khóa video...",
 22 |     "Video Keywords": "Từ Khóa Video (:blue[① Tùy chọn, AI tạo ra ② Sử dụng dấu phẩy **Tiếng Anh** để phân tách, chỉ sử dụng Tiếng Anh])",
 23 |     "Video Settings": "**Cài Đặt Video**",
 24 |     "Video Concat Mode": "Chế Độ Nối Video",
 25 |     "Random": "Nối Ngẫu Nhiên (Được Khuyến Nghị)",
 26 |     "Sequential": "Nối Theo Thứ Tự",
 27 |     "Video Transition Mode": "Chế Độ Chuyển Đổi Video",
 28 |     "None": "Không Có Chuyển Đổi",
 29 |     "Shuffle": "Chuyển Đổi Ngẫu Nhiên",
 30 |     "FadeIn": "FadeIn",
 31 |     "FadeOut": "FadeOut",
 32 |     "SlideIn": "SlideIn",
 33 |     "SlideOut": "SlideOut",
 34 |     "Video Ratio": "Tỷ Lệ Khung Hình Video",
 35 |     "Portrait": "Dọc 9:16",
 36 |     "Landscape": "Ngang 16:9",
 37 |     "Clip Duration": "Thời Lượng Tối Đa Của Đoạn Video (giây)",
 38 |     "Number of Videos Generated Simultaneously": "Số Video Được Tạo Ra Đồng Thời",
 39 |     "Audio Settings": "**Cài Đặt Âm Thanh**",
 40 |     "Speech Synthesis": "Giọng Đọc Văn Bản",
 41 |     "Speech Region": "Vùng(:red[Bắt Buộc,[Lấy Vùng](https://portal.azure.com/#view/Microsoft_Azure_ProjectOxford/CognitiveServicesHub/~/SpeechServices)])",
 42 |     "Speech Key": "Khóa API(:red[Bắt Buộc,[Lấy Khóa API](https://portal.azure.com/#view/Microsoft_Azure_ProjectOxford/CognitiveServicesHub/~/SpeechServices)])",
 43 |     "Speech Volume": "Âm Lượng Giọng Đọc (1.0 đại diện cho 100%)",
 44 |     "Speech Rate": "Tốc độ đọc (1.0 biểu thị tốc độ gốc)",
 45 |     "Male": "Nam",
 46 |     "Female": "Nữ",
 47 |     "Background Music": "Âm Nhạc Nền",
 48 |     "No Background Music": "Không Có Âm Nhạc Nền",
 49 |     "Random Background Music": "Âm Nhạc Nền Ngẫu Nhiên",
 50 |     "Custom Background Music": "Âm Nhạc Nền Tùy Chỉnh",
 51 |     "Custom Background Music File": "Vui lòng nhập đường dẫn tệp cho âm nhạc nền tùy chỉnh:",
 52 |     "Background Music Volume": "Âm Lượng Âm Nhạc Nền (0.2 đại diện cho 20%, âm nhạc nền không nên quá to)",
 53 |     "Subtitle Settings": "**Cài Đặt Phụ Đề**",
 54 |     "Enable Subtitles": "Bật Phụ Đề (Nếu không chọn, các cài đặt dưới đây sẽ không có hiệu lực)",
 55 |     "Font": "Phông Chữ Phụ Đề",
 56 |     "Position": "Vị Trí Phụ Đề",
 57 |     "Top": "Trên",
 58 |     "Center": "Giữa",
 59 |     "Bottom": "Dưới (Được Khuyến Nghị)",
 60 |     "Custom": "Vị trí tùy chỉnh (70, chỉ ra là cách đầu trang 70%)",
 61 |     "Font Size": "Cỡ Chữ Phụ Đề",
 62 |     "Font Color": "Màu Chữ Phụ Đề",
 63 |     "Stroke Color": "Màu Viền Phụ Đề",
 64 |     "Stroke Width": "Độ Rộng Viền Phụ Đề",
 65 |     "Generate Video": "Tạo Video",
 66 |     "Video Script and Subject Cannot Both Be Empty": "Chủ Đề Video và Kịch Bản Video không thể cùng trống",
 67 |     "Generating Video": "Đang tạo video, vui lòng đợi...",
 68 |     "Start Generating Video": "Bắt Đầu Tạo Video",
 69 |     "Video Generation Completed": "Hoàn Tất Tạo Video",
 70 |     "Video Generation Failed": "Tạo Video Thất Bại",
 71 |     "You can download the generated video from the following links": "Bạn có thể tải video được tạo ra từ các liên kết sau",
 72 |     "Basic Settings": "**Cài Đặt Cơ Bản** (:blue[Nhấp để mở rộng])",
 73 |     "Language": "Ngôn Ngữ",
 74 |     "Pexels API Key": "Khóa API Pexels ([Lấy Khóa API](https://www.pexels.com/api/))",
 75 |     "Pixabay API Key": "Khóa API Pixabay ([Lấy Khóa API](https://pixabay.com/api/docs/#api_search_videos))",
 76 |     "LLM Provider": "Nhà Cung Cấp LLM",
 77 |     "API Key": "Khóa API (:red[Bắt Buộc])",
 78 |     "Base Url": "Url Cơ Bản",
 79 |     "Account ID": "ID Tài Khoản (Lấy từ bảng điều khiển Cloudflare)",
 80 |     "Model Name": "Tên Mô Hình",
 81 |     "Please Enter the LLM API Key": "Vui lòng Nhập **Khóa API LLM**",
 82 |     "Please Enter the Pexels API Key": "Vui lòng Nhập **Khóa API Pexels**",
 83 |     "Please Enter the Pixabay API Key": "Vui lòng Nhập **Khóa API Pixabay**",
 84 |     "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",
 85 |     "Video Source": "Nguồn Video",
 86 |     "TikTok": "TikTok (Hỗ trợ TikTok sắp ra mắt)",
 87 |     "Bilibili": "Bilibili (Hỗ trợ Bilibili sắp ra mắt)",
 88 |     "Xiaohongshu": "Xiaohongshu (Hỗ trợ Xiaohongshu sắp ra mắt)",
 89 |     "Local file": "Tệp cục bộ",
 90 |     "Play Voice": "Phát Giọng Nói",
 91 |     "Voice Example": "Đây là văn bản mẫu để kiểm tra tổng hợp giọng nói",
 92 |     "Synthesizing Voice": "Đang tổng hợp giọng nói, vui lòng đợi...",
 93 |     "TTS Provider": "Chọn nhà cung cấp tổng hợp giọng nói",
 94 |     "TTS Servers": "Máy chủ TTS",
 95 |     "No voices available for the selected TTS server. Please select another server.": "Không có giọng nói nào cho máy chủ TTS đã chọn. Vui lòng chọn máy chủ khác.",
 96 |     "SiliconFlow API Key": "Khóa API SiliconFlow",
 97 |     "SiliconFlow TTS Settings": "Cài đặt SiliconFlow TTS",
 98 |     "Speed: Range [0.25, 4.0], default is 1.0": "Tốc độ: Phạm vi [0.25, 4.0], mặc định là 1.0",
 99 |     "Volume: Uses Speech Volume setting, default 1.0 maps to gain 0": "Âm lượng: Sử dụng cài đặt Âm lượng Giọng nói, mặc định 1.0 tương ứng với tăng ích 0",
100 |     "Hide Log": "Ẩn Nhật Ký",
101 |     "Hide Basic Settings": "Ẩn Cài Đặt Cơ Bản\n\nẨn, thanh cài đặt cơ bản sẽ không hiển thị trên trang web.\n\nNếu bạn muốn hiển thị lại, vui lòng đặt `hide_config = false` trong `config.toml`",
102 |     "LLM Settings": "**Cài Đặt LLM**",
103 |     "Video Source Settings": "**Cài Đặt Nguồn Video**"
104 |   }
105 | }


--------------------------------------------------------------------------------
/webui/i18n/zh.json:
--------------------------------------------------------------------------------
  1 | {
  2 |   "Language": "简体中文",
  3 |   "Translation": {
  4 |     "Login Required": "需要登录",
  5 |     "Please login to access settings": "请登录后访问配置设置 (:gray[默认用户名: admin, 密码: admin, 您可以在 config.toml 中修改])",
  6 |     "Username": "用户名",
  7 |     "Password": "密码",
  8 |     "Login": "登录",
  9 |     "Login Error": "登录错误",
 10 |     "Incorrect username or password": "用户名或密码不正确",
 11 |     "Please enter your username and password": "请输入用户名和密码",
 12 |     "Video Script Settings": "**文案设置**",
 13 |     "Video Subject": "视频主题(给定一个关键词,:red[AI自动生成]视频文案)",
 14 |     "Script Language": "生成视频脚本的语言(一般情况AI会自动根据你输入的主题语言输出)",
 15 |     "Generate Video Script and Keywords": "点击使用AI根据**主题**生成 【视频文案】 和 【视频关键词】",
 16 |     "Auto Detect": "自动检测",
 17 |     "Video Script": "视频文案(:blue[①可不填,使用AI生成  ②合理使用标点断句,有助于生成字幕])",
 18 |     "Generate Video Keywords": "点击使用AI根据**文案**生成【视频关键词】",
 19 |     "Please Enter the Video Subject": "请先填写视频文案",
 20 |     "Generating Video Script and Keywords": "AI正在生成视频文案和关键词...",
 21 |     "Generating Video Keywords": "AI正在生成视频关键词...",
 22 |     "Video Keywords": "视频关键词(:blue[①可不填,使用AI生成 ②用**英文逗号**分隔,只支持英文])",
 23 |     "Video Settings": "**视频设置**",
 24 |     "Video Concat Mode": "视频拼接模式",
 25 |     "Random": "随机拼接(推荐)",
 26 |     "Sequential": "顺序拼接",
 27 |     "Video Transition Mode": "视频转场模式",
 28 |     "None": "无转场",
 29 |     "Shuffle": "随机转场",
 30 |     "FadeIn": "渐入",
 31 |     "FadeOut": "渐出",
 32 |     "SlideIn": "滑动入",
 33 |     "SlideOut": "滑动出",
 34 |     "Video Ratio": "视频比例",
 35 |     "Portrait": "竖屏 9:16(抖音视频)",
 36 |     "Landscape": "横屏 16:9(西瓜视频)",
 37 |     "Clip Duration": "视频片段最大时长(秒)(**不是视频总长度**,是指每个**合成片段**的长度)",
 38 |     "Number of Videos Generated Simultaneously": "同时生成视频数量",
 39 |     "Audio Settings": "**音频设置**",
 40 |     "Speech Synthesis": "朗读声音(:red[**与文案语言保持一致**。注意:V2版效果更好,但是需要API KEY])",
 41 |     "Speech Region": "服务区域 (:red[必填,[点击获取](https://portal.azure.com/#view/Microsoft_Azure_ProjectOxford/CognitiveServicesHub/~/SpeechServices)])",
 42 |     "Speech Key": "API Key (:red[必填,密钥1 或 密钥2 均可 [点击获取](https://portal.azure.com/#view/Microsoft_Azure_ProjectOxford/CognitiveServicesHub/~/SpeechServices)])",
 43 |     "Speech Volume": "朗读音量(1.0表示100%)",
 44 |     "Speech Rate": "朗读速度(1.0表示1倍速)",
 45 |     "Male": "男性",
 46 |     "Female": "女性",
 47 |     "Background Music": "背景音乐",
 48 |     "No Background Music": "无背景音乐",
 49 |     "Random Background Music": "随机背景音乐",
 50 |     "Custom Background Music": "自定义背景音乐",
 51 |     "Custom Background Music File": "请输入自定义背景音乐的文件路径",
 52 |     "Background Music Volume": "背景音乐音量(0.2表示20%,背景声音不宜过高)",
 53 |     "Subtitle Settings": "**字幕设置**",
 54 |     "Enable Subtitles": "启用字幕(若取消勾选,下面的设置都将不生效)",
 55 |     "Font": "字幕字体",
 56 |     "Position": "字幕位置",
 57 |     "Top": "顶部",
 58 |     "Center": "中间",
 59 |     "Bottom": "底部(推荐)",
 60 |     "Custom": "自定义位置(70,表示离顶部70%的位置)",
 61 |     "Font Size": "字幕大小",
 62 |     "Font Color": "字幕颜色",
 63 |     "Stroke Color": "描边颜色",
 64 |     "Stroke Width": "描边粗细",
 65 |     "Generate Video": "生成视频",
 66 |     "Video Script and Subject Cannot Both Be Empty": "视频主题 和 视频文案,不能同时为空",
 67 |     "Generating Video": "正在生成视频,请稍候...",
 68 |     "Start Generating Video": "开始生成视频",
 69 |     "Video Generation Completed": "视频生成完成",
 70 |     "Video Generation Failed": "视频生成失败",
 71 |     "You can download the generated video from the following links": "你可以从以下链接下载生成的视频",
 72 |     "Basic Settings": "**基础设置** (:blue[点击展开])",
 73 |     "Language": "界面语言",
 74 |     "Pexels API Key": "Pexels API Key ([点击获取](https://www.pexels.com/api/)) :red[推荐使用]",
 75 |     "Pixabay API Key": "Pixabay API Key ([点击获取](https://pixabay.com/api/docs/#api_search_videos)) :red[可以不用配置,如果 Pexels 无法使用,再选择Pixabay]",
 76 |     "LLM Provider": "大模型提供商",
 77 |     "API Key": "API Key (:red[必填,需要到大模型提供商的后台申请])",
 78 |     "Base Url": "Base Url (可选)",
 79 |     "Account ID": "账户ID (Cloudflare的dash面板url中获取)",
 80 |     "Model Name": "模型名称 (:blue[需要到大模型提供商的后台确认被授权的模型名称])",
 81 |     "Please Enter the LLM API Key": "请先填写大模型 **API Key**",
 82 |     "Please Enter the Pexels API Key": "请先填写 **Pexels API Key**",
 83 |     "Please Enter the Pixabay API Key": "请先填写 **Pixabay API Key**",
 84 |     "Get Help": "有任何问题或建议,可以加入 **微信群** 求助或讨论:https://harryai.cc",
 85 |     "Video Source": "视频来源",
 86 |     "TikTok": "抖音 (TikTok 支持中,敬请期待)",
 87 |     "Bilibili": "哔哩哔哩 (Bilibili 支持中,敬请期待)",
 88 |     "Xiaohongshu": "小红书 (Xiaohongshu 支持中,敬请期待)",
 89 |     "Local file": "本地文件",
 90 |     "Play Voice": "试听语音合成",
 91 |     "Voice Example": "这是一段测试语音合成的示例文本",
 92 |     "Synthesizing Voice": "语音合成中,请稍候...",
 93 |     "TTS Provider": "语音合成提供商",
 94 |     "TTS Servers": "TTS服务器",
 95 |     "No voices available for the selected TTS server. Please select another server.": "当前选择的TTS服务器没有可用的声音,请选择其他服务器。",
 96 |     "SiliconFlow API Key": "硅基流动API密钥 [点击获取](https://cloud.siliconflow.cn/account/ak)",
 97 |     "SiliconFlow TTS Settings": "硅基流动TTS设置",
 98 |     "Speed: Range [0.25, 4.0], default is 1.0": "语速范围 [0.25, 4.0],默认值为1.0",
 99 |     "Volume: Uses Speech Volume setting, default 1.0 maps to gain 0": "音量:使用朗读音量设置,默认值1.0对应增益0",
100 |     "Hide Log": "隐藏日志",
101 |     "Hide Basic Settings": "隐藏基础设置\n\n隐藏后,基础设置面板将不会显示在页面中。\n\n如需要再次显示,请在 `config.toml` 中设置 `hide_config = false`",
102 |     "LLM Settings": "**大模型设置**",
103 |     "Video Source Settings": "**视频源设置**"
104 |   }
105 | }


--------------------------------------------------------------------------------