Compare commits
165 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
09ad53a60e | ||
|
|
bb4333db95 | ||
|
|
dc460c25d2 | ||
|
|
4ca8d8d8ae | ||
|
|
37e56239d9 | ||
|
|
ba8613baeb | ||
|
|
fd8ed3bd69 | ||
|
|
1b8b65f642 | ||
|
|
2128a5fae5 | ||
|
|
2976652509 | ||
|
|
eb2b07b615 | ||
|
|
289c06799a | ||
|
|
afb4eff3e5 | ||
|
|
bdf7af0a12 | ||
|
|
ee931d1933 | ||
|
|
cbd3495426 | ||
|
|
a864be83da | ||
|
|
fee226a149 | ||
|
|
b3f549b4db | ||
|
|
4c21a23a8b | ||
|
|
8d84b5b530 | ||
|
|
6de3d6eedc | ||
|
|
ee680d24cc | ||
|
|
fb3aadeccc | ||
|
|
5254577e5c | ||
|
|
c15a3bdd13 | ||
|
|
d8d8a5b602 | ||
|
|
e258617d4f | ||
|
|
ce64602c08 | ||
|
|
f729124a48 | ||
|
|
367018b3f7 | ||
|
|
b055755689 | ||
|
|
c8894a851b | ||
|
|
28c5bc372f | ||
|
|
7ed2603442 | ||
|
|
1f8b41d2b3 | ||
|
|
ee09cf64d5 | ||
|
|
4e886a1a73 | ||
|
|
661d8cb5ab | ||
|
|
4de02f4429 | ||
|
|
5d06530a39 | ||
|
|
4596804bcf | ||
|
|
c161ab3124 | ||
|
|
376955d4a0 | ||
|
|
b08b79f9cf | ||
|
|
7582022fa9 | ||
|
|
8dbce05344 | ||
|
|
4d91a83858 | ||
|
|
8e93dd7ca0 | ||
|
|
d2277715df | ||
|
|
8861526e0a | ||
|
|
f38fc60394 | ||
|
|
e77bff3ffb | ||
|
|
59a518ce9d | ||
|
|
d922ff2576 | ||
|
|
84b3ef13c0 | ||
|
|
a53da162ac | ||
|
|
e77389ffb5 | ||
|
|
2be09365d9 | ||
|
|
f85c11118d | ||
|
|
419abd760e | ||
|
|
5c0a905c09 | ||
|
|
3c5ef29775 | ||
|
|
bc45d4bcf3 | ||
|
|
e38c79bfad | ||
|
|
c7c7b4847e | ||
|
|
6a5c4e9e73 | ||
|
|
2eb6d4b5cc | ||
|
|
13f3abffd0 | ||
|
|
6164920eaa | ||
|
|
a8d528f41c | ||
|
|
259b3e94fc | ||
|
|
ffcb52fd46 | ||
|
|
ee4337e847 | ||
|
|
16bcec96ee | ||
|
|
cc231e0b62 | ||
|
|
cb2c53334e | ||
|
|
643b0fb30c | ||
|
|
6b97e7dbd4 | ||
|
|
4804aa1e04 | ||
|
|
c04e3988fe | ||
|
|
c9eb16e0a9 | ||
|
|
2d599db892 | ||
|
|
ec50cd0184 | ||
|
|
a1a1a51881 | ||
|
|
0066bab3ec | ||
|
|
2f461d961c | ||
|
|
77c250ce18 | ||
|
|
3f941c8dd9 | ||
|
|
ab5ae7072b | ||
|
|
93da539519 | ||
|
|
5280159f41 | ||
|
|
729c407c30 | ||
|
|
73ec0cf7ad | ||
|
|
a5273b31b3 | ||
|
|
f83c374ef4 | ||
|
|
da010c476f | ||
|
|
78f5ce7cdd | ||
|
|
27553114b0 | ||
|
|
4ae022c059 | ||
|
|
70ed2b5c82 | ||
|
|
89f001742a | ||
|
|
6c8b4f665d | ||
|
|
1c35e50563 | ||
|
|
add34a92f7 | ||
|
|
a44016a7cf | ||
|
|
0521d46826 | ||
|
|
f48fb24dcf | ||
|
|
996cd55462 | ||
|
|
97640d2199 | ||
|
|
f6857d63f9 | ||
|
|
abe12abd7b | ||
|
|
7e8c901fd4 | ||
|
|
1cee2bbb8d | ||
|
|
4d176c6107 | ||
|
|
e499f8a1c4 | ||
|
|
d82b5f76e0 | ||
|
|
414bcb0621 | ||
|
|
d4eb7bc333 | ||
|
|
1e96357f00 | ||
|
|
2e58d7ccf2 | ||
|
|
176660b442 | ||
|
|
b9b9bea2a6 | ||
|
|
17df9a1f27 | ||
|
|
00052b4c50 | ||
|
|
b8369349ea | ||
|
|
3de3e19276 | ||
|
|
bd33419460 | ||
|
|
d13a3cf6e9 | ||
|
|
3e4d5f52fd | ||
|
|
9a1ee9abfb | ||
|
|
2c41e6be62 | ||
|
|
a17d52c1ae | ||
|
|
b1506b9161 | ||
|
|
53923e0d25 | ||
|
|
1a302a1791 | ||
|
|
ce0f557702 | ||
|
|
a8d208bdc3 | ||
|
|
0cb71d6218 | ||
|
|
52b92d175d | ||
|
|
76e1407d9b | ||
|
|
26437a666c | ||
|
|
8907958fec | ||
|
|
0550e433d1 | ||
|
|
1fb3399b02 | ||
|
|
9ab13a74a2 | ||
|
|
1dbfcfadab | ||
|
|
ee7306d216 | ||
|
|
a8b54415a5 | ||
|
|
7a8e25dc36 | ||
|
|
c8adc453ae | ||
|
|
24a9ca514e | ||
|
|
a7466b2393 | ||
|
|
1f2b36a4f0 | ||
|
|
91218ecf95 | ||
|
|
a0a5a4059f | ||
|
|
90f0f560b2 | ||
|
|
05da4a3766 | ||
|
|
066e33def9 | ||
|
|
bb66b7e10c | ||
|
|
1ac643f4d0 | ||
|
|
fea396585f | ||
|
|
3fe6ff42c8 | ||
|
|
a71e54fd7c | ||
|
|
fee660fb8c |
@@ -21,3 +21,4 @@ __pycache__/
|
|||||||
.svn/
|
.svn/
|
||||||
|
|
||||||
storage/
|
storage/
|
||||||
|
config.toml
|
||||||
|
|||||||
33
.github/workflows/gh-pages.yml
vendored
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
name: gp-pages
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
paths:
|
||||||
|
- 'sites/**'
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build-deploy:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
- name: Set-up Node
|
||||||
|
uses: actions/setup-node@v1
|
||||||
|
with:
|
||||||
|
node-version: "18.15.0"
|
||||||
|
- name: Install pnpm
|
||||||
|
run: npm install -g pnpm
|
||||||
|
- name: Install dependencies
|
||||||
|
run: pnpm i
|
||||||
|
working-directory: ./sites
|
||||||
|
- name: Build gh-pages
|
||||||
|
run: pnpm docs:build
|
||||||
|
working-directory: ./sites
|
||||||
|
- name: Deploy to gh-pages
|
||||||
|
uses: crazy-max/ghaction-github-pages@v1
|
||||||
|
with:
|
||||||
|
target_branch: gh-pages
|
||||||
|
build_dir: sites/docs/.vuepress/dist
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.MY_TOKEN }}
|
||||||
10
.gitignore
vendored
@@ -10,3 +10,13 @@
|
|||||||
/*/__pycache__/*
|
/*/__pycache__/*
|
||||||
.vscode
|
.vscode
|
||||||
/**/.streamlit
|
/**/.streamlit
|
||||||
|
__pycache__
|
||||||
|
logs/
|
||||||
|
|
||||||
|
node_modules
|
||||||
|
# VuePress 默认临时文件目录
|
||||||
|
/sites/docs/.vuepress/.temp
|
||||||
|
# VuePress 默认缓存目录
|
||||||
|
/sites/docs/.vuepress/.cache
|
||||||
|
# VuePress 默认构建生成的静态文件目录
|
||||||
|
/sites/docs/.vuepress/dist
|
||||||
|
|||||||
22
CHANGELOG.md
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
<!-- insertion marker -->
|
||||||
|
## [1.1.2](https://github.com/KevinZhang19870314/MoneyPrinterTurbo/releases/tag/1.1.2) - 2024-04-18
|
||||||
|
|
||||||
|
<small>[Compare with first commit](https://github.com/KevinZhang19870314/MoneyPrinterTurbo/compare/d4f7b53b841e65da658e3d77822f9923286ddab6...1.1.2)</small>
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
- add support for maximum concurrency of /api/v1/videos ([abe12ab](https://github.com/KevinZhang19870314/MoneyPrinterTurbo/commit/abe12abd7b78997651468ad5dd656985066f8bd9) by kevin.zhang).
|
||||||
|
- add task deletion endpoint ([d57434e](https://github.com/KevinZhang19870314/MoneyPrinterTurbo/commit/d57434e0d31c8195dbcd3c86ff2763af96736cdf) by kevin.zhang).
|
||||||
|
- add redis support for task state management ([3d45348](https://github.com/KevinZhang19870314/MoneyPrinterTurbo/commit/3d453486627234937c7bfe6f176890360074696b) by kevin.zhang).
|
||||||
|
- enable cors to allow play video through mounted videos url ([3b1871d](https://github.com/KevinZhang19870314/MoneyPrinterTurbo/commit/3b1871d591873594bb4aa8dc17a1253b3a7563a3) by kevin.zhang).
|
||||||
|
- add /api/v1/get_bgm_list and /api/v1/upload_bgm_file ([6d8911f](https://github.com/KevinZhang19870314/MoneyPrinterTurbo/commit/6d8911f5bf496e7c5dd718309a302df88d11817b) by cathy).
|
||||||
|
- return combined videos in /api/v1/tasks response ([28199c9](https://github.com/KevinZhang19870314/MoneyPrinterTurbo/commit/28199c93b78f67e9a6bf50f290f1591078f63da8) by cathy).
|
||||||
|
- add Dockerfile ([f3b3c7f](https://github.com/KevinZhang19870314/MoneyPrinterTurbo/commit/f3b3c7fb47b01ed4ecba44eaebf29f5d6d2cb7b5) by kevin.zhang).
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
- response parsing bug for gemini ([ee7306d](https://github.com/KevinZhang19870314/MoneyPrinterTurbo/commit/ee7306d216ea41e40855bbca396cacb094d572db) by elf-mouse).
|
||||||
|
|
||||||
|
### Code Refactoring
|
||||||
|
|
||||||
|
- Streaming MP4 files in the browser using video html element instead of waiting for the entire file to download before playing ([d13a3cf](https://github.com/KevinZhang19870314/MoneyPrinterTurbo/commit/d13a3cf6e911d1573c62b1f6459c3c0b7a1bc18d) by kevin.zhang).
|
||||||
12
Dockerfile
@@ -1,9 +1,12 @@
|
|||||||
# Use an official Python runtime as a parent image
|
# Use an official Python runtime as a parent image
|
||||||
FROM python:3.10-slim
|
FROM python:3.10-slim-bullseye
|
||||||
|
|
||||||
# Set the working directory in the container
|
# Set the working directory in the container
|
||||||
WORKDIR /MoneyPrinterTurbo
|
WORKDIR /MoneyPrinterTurbo
|
||||||
|
|
||||||
|
# 设置/MoneyPrinterTurbo目录权限为777
|
||||||
|
RUN chmod 777 /MoneyPrinterTurbo
|
||||||
|
|
||||||
ENV PYTHONPATH="/MoneyPrinterTurbo"
|
ENV PYTHONPATH="/MoneyPrinterTurbo"
|
||||||
|
|
||||||
# Install system dependencies
|
# Install system dependencies
|
||||||
@@ -16,12 +19,15 @@ RUN apt-get update && apt-get install -y \
|
|||||||
# Fix security policy for ImageMagick
|
# Fix security policy for ImageMagick
|
||||||
RUN sed -i '/<policy domain="path" rights="none" pattern="@\*"/d' /etc/ImageMagick-6/policy.xml
|
RUN sed -i '/<policy domain="path" rights="none" pattern="@\*"/d' /etc/ImageMagick-6/policy.xml
|
||||||
|
|
||||||
# Copy the current directory contents into the container at /MoneyPrinterTurbo
|
# Copy only the requirements.txt first to leverage Docker cache
|
||||||
COPY . .
|
COPY requirements.txt ./
|
||||||
|
|
||||||
# Install Python dependencies
|
# Install Python dependencies
|
||||||
RUN pip install --no-cache-dir -r requirements.txt
|
RUN pip install --no-cache-dir -r requirements.txt
|
||||||
|
|
||||||
|
# Now copy the rest of the codebase into the image
|
||||||
|
COPY . .
|
||||||
|
|
||||||
# Expose the port the app runs on
|
# Expose the port the app runs on
|
||||||
EXPOSE 8501
|
EXPOSE 8501
|
||||||
|
|
||||||
|
|||||||
40
README-en.md
@@ -10,9 +10,9 @@
|
|||||||
|
|
||||||
<h3>English | <a href="README.md">简体中文</a></h3>
|
<h3>English | <a href="README.md">简体中文</a></h3>
|
||||||
|
|
||||||
|
<div align="center">
|
||||||
> Thanks to [RootFTW](https://github.com/Root-FTW) for the translation
|
<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>
|
||||||
|
</div>
|
||||||
|
|
||||||
Simply provide a <b>topic</b> or <b>keyword</b> for a video, and it will automatically generate the video copy, video
|
Simply provide a <b>topic</b> or <b>keyword</b> for a video, and it will automatically generate the video copy, video
|
||||||
materials, video subtitles, and video background music before synthesizing a high-definition short video.
|
materials, video subtitles, and video background music before synthesizing a high-definition short video.
|
||||||
@@ -59,8 +59,7 @@ https://reccloud.com
|
|||||||
- [x] Supports integration with various models such as **OpenAI**, **moonshot**, **Azure**, **gpt4free**, **one-api**,
|
- [x] Supports integration with various models such as **OpenAI**, **moonshot**, **Azure**, **gpt4free**, **one-api**,
|
||||||
**qianwen**, **Google Gemini**, **Ollama** and more
|
**qianwen**, **Google Gemini**, **Ollama** and more
|
||||||
|
|
||||||
❓[How to Use the Free OpenAI GPT-3.5 Model?](https://github.com/harry0703/MoneyPrinterTurbo/blob/main/README-en.md#common-questions-)
|
❓[How to Use the Free OpenAI GPT-3.5 Model?](https://github.com/harry0703/MoneyPrinterTurbo/blob/main/README-en.md#common-questions-)
|
||||||
|
|
||||||
|
|
||||||
### Future Plans 📅
|
### Future Plans 📅
|
||||||
|
|
||||||
@@ -111,6 +110,11 @@ https://reccloud.com
|
|||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
|
## System Requirements 📦
|
||||||
|
|
||||||
|
- Recommended minimum 4 CPU cores or more, 8G of memory or more, GPU is not required
|
||||||
|
- Windows 10 or MacOS 11.0, and their later versions
|
||||||
|
|
||||||
## Installation & Deployment 📥
|
## Installation & Deployment 📥
|
||||||
|
|
||||||
- Try to avoid using **Chinese paths** to prevent unpredictable issues
|
- Try to avoid using **Chinese paths** to prevent unpredictable issues
|
||||||
@@ -245,8 +249,28 @@ You can switch between them by modifying the `subtitle_provider` in the `config.
|
|||||||
It is recommended to use `edge` mode, and switch to `whisper` mode if the quality of the subtitles generated is not
|
It is recommended to use `edge` mode, and switch to `whisper` mode if the quality of the subtitles generated is not
|
||||||
satisfactory.
|
satisfactory.
|
||||||
|
|
||||||
|
> Note:
|
||||||
> If left blank, it means no subtitles will be generated.
|
> If left blank, it means no subtitles will be generated.
|
||||||
|
|
||||||
|
**Download whisper**
|
||||||
|
- Please ensure a good internet connectivity
|
||||||
|
- `whisper` model can be downloaded from HuggingFace: https://huggingface.co/openai/whisper-large-v3/tree/main
|
||||||
|
|
||||||
|
After downloading the model to local machine, copy the whole folder and put it into the following path: `.\MoneyPrinterTurbo\models`
|
||||||
|
|
||||||
|
This is what the final path should look like: `.\MoneyPrinterTurbo\models\whisper-large-v3`
|
||||||
|
|
||||||
|
```
|
||||||
|
MoneyPrinterTurbo
|
||||||
|
├─models
|
||||||
|
│ └─whisper-large-v3
|
||||||
|
│ config.json
|
||||||
|
│ model.bin
|
||||||
|
│ preprocessor_config.json
|
||||||
|
│ tokenizer.json
|
||||||
|
│ vocabulary.json
|
||||||
|
```
|
||||||
|
|
||||||
## Background Music 🎵
|
## Background Music 🎵
|
||||||
|
|
||||||
Background music for videos is located in the project's `resource/songs` directory.
|
Background music for videos is located in the project's `resource/songs` directory.
|
||||||
@@ -261,12 +285,16 @@ own fonts.
|
|||||||
## Common Questions 🤔
|
## Common Questions 🤔
|
||||||
|
|
||||||
### ❓How to Use the Free OpenAI GPT-3.5 Model?
|
### ❓How to Use the Free OpenAI GPT-3.5 Model?
|
||||||
[OpenAI has announced that ChatGPT with 3.5 is now free](https://openai.com/blog/start-using-chatgpt-instantly), and developers have wrapped it into an API for direct usage.
|
|
||||||
|
[OpenAI has announced that ChatGPT with 3.5 is now free](https://openai.com/blog/start-using-chatgpt-instantly), and
|
||||||
|
developers have wrapped it into an API for direct usage.
|
||||||
|
|
||||||
**Ensure you have Docker installed and running**. Execute the following command to start the Docker service:
|
**Ensure you have Docker installed and running**. Execute the following command to start the Docker service:
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
docker run -p 3040:3040 missuo/freegpt35
|
docker run -p 3040:3040 missuo/freegpt35
|
||||||
```
|
```
|
||||||
|
|
||||||
Once successfully started, modify the `config.toml` configuration as follows:
|
Once successfully started, modify the `config.toml` configuration as follows:
|
||||||
|
|
||||||
- Set `llm_provider` to `openai`
|
- Set `llm_provider` to `openai`
|
||||||
|
|||||||
220
README.md
@@ -9,6 +9,9 @@
|
|||||||
</p>
|
</p>
|
||||||
<br>
|
<br>
|
||||||
<h3>简体中文 | <a href="README-en.md">English</a></h3>
|
<h3>简体中文 | <a href="README-en.md">English</a></h3>
|
||||||
|
<div align="center">
|
||||||
|
<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>
|
||||||
|
</div>
|
||||||
<br>
|
<br>
|
||||||
只需提供一个视频 <b>主题</b> 或 <b>关键词</b> ,就可以全自动生成视频文案、视频素材、视频字幕、视频背景音乐,然后合成一个高清的短视频。
|
只需提供一个视频 <b>主题</b> 或 <b>关键词</b> ,就可以全自动生成视频文案、视频素材、视频字幕、视频背景音乐,然后合成一个高清的短视频。
|
||||||
<br>
|
<br>
|
||||||
@@ -26,7 +29,6 @@
|
|||||||
## 特别感谢 🙏
|
## 特别感谢 🙏
|
||||||
|
|
||||||
由于该项目的 **部署** 和 **使用**,对于一些小白用户来说,还是 **有一定的门槛**,在此特别感谢
|
由于该项目的 **部署** 和 **使用**,对于一些小白用户来说,还是 **有一定的门槛**,在此特别感谢
|
||||||
|
|
||||||
**录咖(AI智能 多媒体服务平台)** 网站基于该项目,提供的免费`AI视频生成器`服务,可以不用部署,直接在线使用,非常方便。
|
**录咖(AI智能 多媒体服务平台)** 网站基于该项目,提供的免费`AI视频生成器`服务,可以不用部署,直接在线使用,非常方便。
|
||||||
|
|
||||||
- 中文版:https://reccloud.cn
|
- 中文版:https://reccloud.cn
|
||||||
@@ -34,6 +36,14 @@
|
|||||||
|
|
||||||

|

|
||||||
|
|
||||||
|
## 感谢赞助 🙏
|
||||||
|
|
||||||
|
感谢佐糖 https://picwish.cn 对该项目的支持和赞助,使得该项目能够持续的更新和维护。
|
||||||
|
|
||||||
|
佐糖专注于**图像处理领域**,提供丰富的**图像处理工具**,将复杂操作极致简化,真正实现让图像处理更简单。
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
## 功能特性 🎯
|
## 功能特性 🎯
|
||||||
|
|
||||||
- [x] 完整的 **MVC架构**,代码 **结构清晰**,易于维护,支持 `API` 和 `Web界面`
|
- [x] 完整的 **MVC架构**,代码 **结构清晰**,易于维护,支持 `API` 和 `Web界面`
|
||||||
@@ -42,15 +52,15 @@
|
|||||||
- [x] 竖屏 9:16,`1080x1920`
|
- [x] 竖屏 9:16,`1080x1920`
|
||||||
- [x] 横屏 16:9,`1920x1080`
|
- [x] 横屏 16:9,`1920x1080`
|
||||||
- [x] 支持 **批量视频生成**,可以一次生成多个视频,然后选择一个最满意的
|
- [x] 支持 **批量视频生成**,可以一次生成多个视频,然后选择一个最满意的
|
||||||
- [x] 支持 **视频片段时长**设置,方便调节素材切换频率
|
- [x] 支持 **视频片段时长** 设置,方便调节素材切换频率
|
||||||
- [x] 支持 **中文** 和 **英文** 视频文案
|
- [x] 支持 **中文** 和 **英文** 视频文案
|
||||||
- [x] 支持 **多种语音** 合成
|
- [x] 支持 **多种语音** 合成,可 **实时试听** 效果
|
||||||
- [x] 支持 **字幕生成**,可以调整 `字体`、`位置`、`颜色`、`大小`,同时支持`字幕描边`设置
|
- [x] 支持 **字幕生成**,可以调整 `字体`、`位置`、`颜色`、`大小`,同时支持`字幕描边`设置
|
||||||
- [x] 支持 **背景音乐**,随机或者指定音乐文件,可设置`背景音乐音量`
|
- [x] 支持 **背景音乐**,随机或者指定音乐文件,可设置`背景音乐音量`
|
||||||
- [x] 视频素材来源 **高清**,而且 **无版权**
|
- [x] 视频素材来源 **高清**,而且 **无版权**,也可以使用自己的 **本地素材**
|
||||||
- [x] 支持 **OpenAI**、**moonshot**、**Azure**、**gpt4free**、**one-api**、**通义千问**、**Google Gemini**、**Ollama** 等多种模型接入
|
- [x] 支持 **OpenAI**、**Moonshot**、**Azure**、**gpt4free**、**one-api**、**通义千问**、**Google Gemini**、**Ollama**、
|
||||||
|
**DeepSeek** 等多种模型接入
|
||||||
❓[如何使用免费的 **OpenAI GPT-3.5** 模型?](https://github.com/harry0703/MoneyPrinterTurbo?tab=readme-ov-file#%E5%B8%B8%E8%A7%81%E9%97%AE%E9%A2%98-)
|
- 中国用户建议使用 **DeepSeek** 或 **Moonshot** 作为大模型提供商(国内可直接访问,不需要VPN。注册就送额度,基本够用)
|
||||||
|
|
||||||
### 后期计划 📅
|
### 后期计划 📅
|
||||||
|
|
||||||
@@ -59,13 +69,13 @@
|
|||||||
- [ ] 增加视频转场效果,使其看起来更加的流畅
|
- [ ] 增加视频转场效果,使其看起来更加的流畅
|
||||||
- [ ] 增加更多视频素材来源,优化视频素材和文案的匹配度
|
- [ ] 增加更多视频素材来源,优化视频素材和文案的匹配度
|
||||||
- [ ] 增加视频长度选项:短、中、长
|
- [ ] 增加视频长度选项:短、中、长
|
||||||
- [ ] 打包成一键启动包(Windows,macOS),方便使用
|
- [ ] 支持更多的语音合成服务商,比如 OpenAI TTS
|
||||||
- [ ] 增加免费网络代理,让访问OpenAI和素材下载不再受限
|
|
||||||
- [ ] 可以使用自己的素材
|
|
||||||
- [ ] 朗读声音和背景音乐,提供实时试听
|
|
||||||
- [ ] 支持更多的语音合成服务商,比如 OpenAI TTS, Azure TTS
|
|
||||||
- [ ] 自动上传到YouTube平台
|
- [ ] 自动上传到YouTube平台
|
||||||
|
|
||||||
|
## 交流讨论 💬
|
||||||
|
|
||||||
|
<img src="docs/wechat-group.jpg" width="250">
|
||||||
|
|
||||||
## 视频演示 📺
|
## 视频演示 📺
|
||||||
|
|
||||||
### 竖屏 9:16
|
### 竖屏 9:16
|
||||||
@@ -74,12 +84,14 @@
|
|||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th align="center"><g-emoji class="g-emoji" alias="arrow_forward">▶️</g-emoji> 《如何增加生活的乐趣》</th>
|
<th align="center"><g-emoji class="g-emoji" alias="arrow_forward">▶️</g-emoji> 《如何增加生活的乐趣》</th>
|
||||||
|
<th align="center"><g-emoji class="g-emoji" alias="arrow_forward">▶️</g-emoji> 《金钱的作用》<br>更真实的合成声音</th>
|
||||||
<th align="center"><g-emoji class="g-emoji" alias="arrow_forward">▶️</g-emoji> 《生命的意义是什么》</th>
|
<th align="center"><g-emoji class="g-emoji" alias="arrow_forward">▶️</g-emoji> 《生命的意义是什么》</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr>
|
<tr>
|
||||||
<td align="center"><video src="https://github.com/harry0703/MoneyPrinterTurbo/assets/4928832/a84d33d5-27a2-4aba-8fd0-9fb2bd91c6a6"></video></td>
|
<td align="center"><video src="https://github.com/harry0703/MoneyPrinterTurbo/assets/4928832/a84d33d5-27a2-4aba-8fd0-9fb2bd91c6a6"></video></td>
|
||||||
|
<td align="center"><video src="https://github.com/harry0703/MoneyPrinterTurbo/assets/4928832/af2f3b0b-002e-49fe-b161-18ba91c055e8"></video></td>
|
||||||
<td align="center"><video src="https://github.com/harry0703/MoneyPrinterTurbo/assets/4928832/112c9564-d52b-4472-99ad-970b75f66476"></video></td>
|
<td align="center"><video src="https://github.com/harry0703/MoneyPrinterTurbo/assets/4928832/112c9564-d52b-4472-99ad-970b75f66476"></video></td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
@@ -102,8 +114,31 @@
|
|||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
|
## 配置要求 📦
|
||||||
|
|
||||||
|
- 建议最低 CPU 4核或以上,内存 8G 或以上,显卡非必须
|
||||||
|
- Windows 10 或 MacOS 11.0 以上系统
|
||||||
|
|
||||||
|
## 快速开始 🚀
|
||||||
|
|
||||||
|
下载一键启动包,解压直接使用(路径不要有 **中文** 和 **空格**)
|
||||||
|
|
||||||
|
### Windows
|
||||||
|
|
||||||
|
- 百度网盘: https://pan.baidu.com/s/1jKF1mgsjfN8fBk6uTEHArQ?pwd=jrp7 提取码: jrp7
|
||||||
|
|
||||||
|
下载后,建议先**双击执行** `update.bat` 更新到**最新代码**,然后双击 `start.bat` 启动
|
||||||
|
|
||||||
|
启动后,会自动打开浏览器(如果打开是空白,建议换成 **Chrome** 或者 **Edge** 打开)
|
||||||
|
|
||||||
|
### 其他系统
|
||||||
|
|
||||||
|
还没有制作一键启动包,看下面的 **安装部署** 部分,建议使用 **docker** 部署,更加方便。
|
||||||
|
|
||||||
## 安装部署 📥
|
## 安装部署 📥
|
||||||
|
|
||||||
|
### 前提条件
|
||||||
|
|
||||||
- 尽量不要使用 **中文路径**,避免出现一些无法预料的问题
|
- 尽量不要使用 **中文路径**,避免出现一些无法预料的问题
|
||||||
- 请确保你的 **网络** 是正常的,VPN需要打开`全局流量`模式
|
- 请确保你的 **网络** 是正常的,VPN需要打开`全局流量`模式
|
||||||
|
|
||||||
@@ -119,14 +154,6 @@ git clone https://github.com/harry0703/MoneyPrinterTurbo.git
|
|||||||
- 按照 `config.toml` 文件中的说明,配置好 `pexels_api_keys` 和 `llm_provider`,并根据 llm_provider 对应的服务商,配置相关的
|
- 按照 `config.toml` 文件中的说明,配置好 `pexels_api_keys` 和 `llm_provider`,并根据 llm_provider 对应的服务商,配置相关的
|
||||||
API Key
|
API Key
|
||||||
|
|
||||||
#### ③ 配置大模型(LLM)
|
|
||||||
|
|
||||||
- 如果要使用 `GPT-4.0` 或 `GPT-3.5`,需要有 `OpenAI` 的 `API Key`,如果没有,可以将 `llm_provider` 设置为 `g4f` (
|
|
||||||
一个免费使用GPT的开源库 https://github.com/xtekky/gpt4free ,但是该免费的服务,稳定性较差,有时候可以用,有时候用不了)
|
|
||||||
- 或者可以使用到 [月之暗面](https://platform.moonshot.cn/console/api-keys) 申请。注册就送
|
|
||||||
15元体验金,可以对话1500次左右。然后设置 `llm_provider="moonshot"` 和 `moonshot_api_key`
|
|
||||||
- 也可以使用 通义千问,具体请看配置文件里面的注释说明
|
|
||||||
|
|
||||||
### Docker部署 🐳
|
### Docker部署 🐳
|
||||||
|
|
||||||
#### ① 启动Docker
|
#### ① 启动Docker
|
||||||
@@ -134,6 +161,7 @@ git clone https://github.com/harry0703/MoneyPrinterTurbo.git
|
|||||||
如果未安装 Docker,请先安装 https://www.docker.com/products/docker-desktop/
|
如果未安装 Docker,请先安装 https://www.docker.com/products/docker-desktop/
|
||||||
|
|
||||||
如果是Windows系统,请参考微软的文档:
|
如果是Windows系统,请参考微软的文档:
|
||||||
|
|
||||||
1. https://learn.microsoft.com/zh-cn/windows/wsl/install
|
1. https://learn.microsoft.com/zh-cn/windows/wsl/install
|
||||||
2. https://learn.microsoft.com/zh-cn/windows/wsl/tutorials/wsl-containers
|
2. https://learn.microsoft.com/zh-cn/windows/wsl/tutorials/wsl-containers
|
||||||
|
|
||||||
@@ -171,29 +199,24 @@ pip install -r requirements.txt
|
|||||||
|
|
||||||
#### ② 安装好 ImageMagick
|
#### ② 安装好 ImageMagick
|
||||||
|
|
||||||
###### Windows:
|
- Windows:
|
||||||
|
- 下载 https://imagemagick.org/script/download.php 选择Windows版本,切记一定要选择 **静态库** 版本,比如
|
||||||
|
ImageMagick-7.1.1-32-Q16-x64-**static**.exe
|
||||||
|
- 安装下载好的 ImageMagick,**注意不要修改安装路径**
|
||||||
|
- 修改 `配置文件 config.toml` 中的 `imagemagick_path` 为你的 **实际安装路径**
|
||||||
|
|
||||||
- 下载 https://imagemagick.org/archive/binaries/ImageMagick-7.1.1-30-Q16-x64-static.exe
|
- MacOS:
|
||||||
- 安装下载好的 ImageMagick,注意不要修改安装路径
|
```shell
|
||||||
- 修改 `配置文件 config.toml` 中的 `imagemagick_path` 为你的实际安装路径(如果安装的时候没有修改路径,直接取消注释即可)
|
brew install imagemagick
|
||||||
|
````
|
||||||
###### MacOS:
|
- Ubuntu
|
||||||
|
```shell
|
||||||
```shell
|
sudo apt-get install imagemagick
|
||||||
brew install imagemagick
|
```
|
||||||
````
|
- CentOS
|
||||||
|
```shell
|
||||||
###### Ubuntu
|
sudo yum install ImageMagick
|
||||||
|
```
|
||||||
```shell
|
|
||||||
sudo apt-get install imagemagick
|
|
||||||
```
|
|
||||||
|
|
||||||
###### CentOS
|
|
||||||
|
|
||||||
```shell
|
|
||||||
sudo yum install ImageMagick
|
|
||||||
```
|
|
||||||
|
|
||||||
#### ③ 启动Web界面 🌐
|
#### ③ 启动Web界面 🌐
|
||||||
|
|
||||||
@@ -212,7 +235,8 @@ webui.bat
|
|||||||
conda activate MoneyPrinterTurbo
|
conda activate MoneyPrinterTurbo
|
||||||
sh webui.sh
|
sh webui.sh
|
||||||
```
|
```
|
||||||
启动后,会自动打开浏览器
|
|
||||||
|
启动后,会自动打开浏览器(如果打开是空白,建议换成 **Chrome** 或者 **Edge** 打开)
|
||||||
|
|
||||||
#### ④ 启动API服务 🚀
|
#### ④ 启动API服务 🚀
|
||||||
|
|
||||||
@@ -226,21 +250,45 @@ python main.py
|
|||||||
|
|
||||||
所有支持的声音列表,可以查看:[声音列表](./docs/voice-list.txt)
|
所有支持的声音列表,可以查看:[声音列表](./docs/voice-list.txt)
|
||||||
|
|
||||||
|
2024-04-16 v1.1.2 新增了9种Azure的语音合成声音,需要配置API KEY,该声音合成的更加真实。
|
||||||
|
|
||||||
## 字幕生成 📜
|
## 字幕生成 📜
|
||||||
|
|
||||||
当前支持2种字幕生成方式:
|
当前支持2种字幕生成方式:
|
||||||
|
|
||||||
- edge: 生成速度更快,性能更好,对电脑配置没有要求,但是质量可能不稳定
|
- **edge**: 生成`速度快`,性能更好,对电脑配置没有要求,但是质量可能不稳定
|
||||||
- whisper: 生成速度较慢,性能较差,对电脑配置有一定要求,但是质量更可靠。
|
- **whisper**: 生成`速度慢`,性能较差,对电脑配置有一定要求,但是`质量更可靠`。
|
||||||
|
|
||||||
可以修改 `config.toml` 配置文件中的 `subtitle_provider` 进行切换
|
可以修改 `config.toml` 配置文件中的 `subtitle_provider` 进行切换
|
||||||
|
|
||||||
建议使用 `edge` 模式,如果生成的字幕质量不好,再切换到 `whisper` 模式
|
建议使用 `edge` 模式,如果生成的字幕质量不好,再切换到 `whisper` 模式
|
||||||
|
|
||||||
> 注意:
|
> 注意:
|
||||||
|
|
||||||
1. whisper 模式下需要到 HuggingFace 下载一个模型文件,大约 3GB 左右,请确保网络通畅
|
1. whisper 模式下需要到 HuggingFace 下载一个模型文件,大约 3GB 左右,请确保网络通畅
|
||||||
2. 如果留空,表示不生成字幕。
|
2. 如果留空,表示不生成字幕。
|
||||||
|
|
||||||
|
> 由于国内无法访问 HuggingFace,可以使用以下方法下载 `whisper-large-v3` 的模型文件
|
||||||
|
|
||||||
|
下载地址:
|
||||||
|
|
||||||
|
- 百度网盘: https://pan.baidu.com/s/11h3Q6tsDtjQKTjUu3sc5cA?pwd=xjs9
|
||||||
|
- 夸克网盘:https://pan.quark.cn/s/3ee3d991d64b
|
||||||
|
|
||||||
|
模型下载后解压,整个目录放到 `.\MoneyPrinterTurbo\models` 里面,
|
||||||
|
最终的文件路径应该是这样: `.\MoneyPrinterTurbo\models\whisper-large-v3`
|
||||||
|
|
||||||
|
```
|
||||||
|
MoneyPrinterTurbo
|
||||||
|
├─models
|
||||||
|
│ └─whisper-large-v3
|
||||||
|
│ config.json
|
||||||
|
│ model.bin
|
||||||
|
│ preprocessor_config.json
|
||||||
|
│ tokenizer.json
|
||||||
|
│ vocabulary.json
|
||||||
|
```
|
||||||
|
|
||||||
## 背景音乐 🎵
|
## 背景音乐 🎵
|
||||||
|
|
||||||
用于视频的背景音乐,位于项目的 `resource/songs` 目录下。
|
用于视频的背景音乐,位于项目的 `resource/songs` 目录下。
|
||||||
@@ -253,25 +301,32 @@ python main.py
|
|||||||
## 常见问题 🤔
|
## 常见问题 🤔
|
||||||
|
|
||||||
### ❓如何使用免费的OpenAI GPT-3.5模型?
|
### ❓如何使用免费的OpenAI GPT-3.5模型?
|
||||||
|
|
||||||
[OpenAI宣布ChatGPT里面3.5已经免费了](https://openai.com/blog/start-using-chatgpt-instantly),有开发者将其封装成了API,可以直接调用
|
[OpenAI宣布ChatGPT里面3.5已经免费了](https://openai.com/blog/start-using-chatgpt-instantly),有开发者将其封装成了API,可以直接调用
|
||||||
|
|
||||||
**确保你安装和启动了docker服务**,执行以下命令启动docker服务
|
**确保你安装和启动了docker服务**,执行以下命令启动docker服务
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
docker run -p 3040:3040 missuo/freegpt35
|
docker run -p 3040:3040 missuo/freegpt35
|
||||||
```
|
```
|
||||||
|
|
||||||
启动成功后,修改 `config.toml` 中的配置
|
启动成功后,修改 `config.toml` 中的配置
|
||||||
|
|
||||||
- `llm_provider` 设置为 `openai`
|
- `llm_provider` 设置为 `openai`
|
||||||
- `openai_api_key` 随便填写一个即可,比如 '123456'
|
- `openai_api_key` 随便填写一个即可,比如 '123456'
|
||||||
- `openai_base_url` 改为 `http://localhost:3040/v1/`
|
- `openai_base_url` 改为 `http://localhost:3040/v1/`
|
||||||
- `openai_model_name` 改为 `gpt-3.5-turbo`
|
- `openai_model_name` 改为 `gpt-3.5-turbo`
|
||||||
|
|
||||||
|
> 注意:该方式稳定性较差
|
||||||
|
|
||||||
### ❓AttributeError: 'str' object has no attribute 'choices'`
|
### ❓AttributeError: 'str' object has no attribute 'choices'`
|
||||||
|
|
||||||
这个问题是由于 OpenAI 或者其他 LLM,没有返回正确的回复导致的。
|
这个问题是由于大模型没有返回正确的回复导致的。
|
||||||
|
|
||||||
大概率是网络原因, 使用 **VPN**,或者设置 `openai_base_url` 为你的代理 ,应该就可以解决了。
|
大概率是网络原因, 使用 **VPN**,或者设置 `openai_base_url` 为你的代理 ,应该就可以解决了。
|
||||||
|
|
||||||
|
同时建议使用 **Moonshot** 或 **DeepSeek** 作为大模型提供商,这两个服务商在国内访问速度更快,更加稳定。
|
||||||
|
|
||||||
### ❓RuntimeError: No ffmpeg exe could be found
|
### ❓RuntimeError: No ffmpeg exe could be found
|
||||||
|
|
||||||
通常情况下,ffmpeg 会被自动下载,并且会被自动检测到。
|
通常情况下,ffmpeg 会被自动下载,并且会被自动检测到。
|
||||||
@@ -290,52 +345,14 @@ Install ffmpeg on your system, or set the IMAGEIO_FFMPEG_EXE environment variabl
|
|||||||
ffmpeg_path = "C:\\Users\\harry\\Downloads\\ffmpeg.exe"
|
ffmpeg_path = "C:\\Users\\harry\\Downloads\\ffmpeg.exe"
|
||||||
```
|
```
|
||||||
|
|
||||||
### ❓生成音频时报错或下载视频报错
|
|
||||||
|
|
||||||
[issue 56](https://github.com/harry0703/MoneyPrinterTurbo/issues/56)
|
|
||||||
|
|
||||||
```
|
|
||||||
failed to generate audio, maybe the network is not available.
|
|
||||||
if you are in China, please use a VPN.
|
|
||||||
```
|
|
||||||
|
|
||||||
[issue 44](https://github.com/harry0703/MoneyPrinterTurbo/issues/44)
|
|
||||||
|
|
||||||
```
|
|
||||||
failed to download videos, maybe the network is not available.
|
|
||||||
if you are in China, please use a VPN.
|
|
||||||
```
|
|
||||||
|
|
||||||
这个大概率是网络原因,无法访问境外的服务,请使用VPN解决。
|
|
||||||
|
|
||||||
### ❓ImageMagick is not installed on your computer
|
|
||||||
|
|
||||||
[issue 33](https://github.com/harry0703/MoneyPrinterTurbo/issues/33)
|
|
||||||
|
|
||||||
1. 按照 `示例配置` 里面提供的 `下载地址`
|
|
||||||
,安装 https://imagemagick.org/archive/binaries/ImageMagick-7.1.1-29-Q16-x64-static.exe, 用静态库
|
|
||||||
2. 不要安装在中文路径里面,避免出现一些无法预料的问题
|
|
||||||
|
|
||||||
[issue 54](https://github.com/harry0703/MoneyPrinterTurbo/issues/54#issuecomment-2017842022)
|
|
||||||
|
|
||||||
如果是linux系统,可以手动安装,参考 https://cn.linux-console.net/?p=16978
|
|
||||||
|
|
||||||
感谢 [@wangwenqiao666](https://github.com/wangwenqiao666)的研究探索
|
|
||||||
|
|
||||||
### ❓ImageMagick的安全策略阻止了与临时文件@/tmp/tmpur5hyyto.txt相关的操作
|
### ❓ImageMagick的安全策略阻止了与临时文件@/tmp/tmpur5hyyto.txt相关的操作
|
||||||
|
|
||||||
[issue 92](https://github.com/harry0703/MoneyPrinterTurbo/issues/92)
|
|
||||||
|
|
||||||
可以在ImageMagick的配置文件policy.xml中找到这些策略。
|
可以在ImageMagick的配置文件policy.xml中找到这些策略。
|
||||||
这个文件通常位于 /etc/ImageMagick-`X`/ 或 ImageMagick 安装目录的类似位置。
|
这个文件通常位于 /etc/ImageMagick-`X`/ 或 ImageMagick 安装目录的类似位置。
|
||||||
修改包含`pattern="@"`的条目,将`rights="none"`更改为`rights="read|write"`以允许对文件的读写操作。
|
修改包含`pattern="@"`的条目,将`rights="none"`更改为`rights="read|write"`以允许对文件的读写操作。
|
||||||
|
|
||||||
感谢 [@chenhengzh](https://github.com/chenhengzh)的研究探索
|
|
||||||
|
|
||||||
### ❓OSError: [Errno 24] Too many open files
|
### ❓OSError: [Errno 24] Too many open files
|
||||||
|
|
||||||
[issue 100](https://github.com/harry0703/MoneyPrinterTurbo/issues/100)
|
|
||||||
|
|
||||||
这个问题是由于系统打开文件数限制导致的,可以通过修改系统的文件打开数限制来解决。
|
这个问题是由于系统打开文件数限制导致的,可以通过修改系统的文件打开数限制来解决。
|
||||||
|
|
||||||
查看当前限制
|
查看当前限制
|
||||||
@@ -350,39 +367,25 @@ ulimit -n
|
|||||||
ulimit -n 10240
|
ulimit -n 10240
|
||||||
```
|
```
|
||||||
|
|
||||||
### ❓AttributeError: module 'PIL.Image' has no attribute 'ANTIALIAS'
|
### ❓Whisper 模型下载失败,出现如下错误
|
||||||
|
|
||||||
[issue 101](https://github.com/harry0703/MoneyPrinterTurbo/issues/101),
|
LocalEntryNotfoundEror: Cannot find an appropriate cached snapshotfolderfor the specified revision on the local disk and
|
||||||
[issue 83](https://github.com/harry0703/MoneyPrinterTurbo/issues/83),
|
outgoing trafic has been disabled.
|
||||||
[issue 70](https://github.com/harry0703/MoneyPrinterTurbo/issues/70)
|
To enablerepo look-ups and downloads online, pass 'local files only=False' as input.
|
||||||
|
|
||||||
先看下当前的 Pillow 版本是多少
|
或者
|
||||||
|
|
||||||
```shell
|
An error occured while synchronizing the model Systran/faster-whisper-large-v3 from the Hugging Face Hub:
|
||||||
pip list |grep Pillow
|
An error happened while trying to locate the files on the Hub and we cannot find the appropriate snapshot folder for the
|
||||||
```
|
specified revision on the local disk. Please check your internet connection and try again.
|
||||||
|
Trying to load the model directly from the local cache, if it exists.
|
||||||
|
|
||||||
如果是 10.x 的版本,可以尝试下降级看看,有用户反馈降级后正常
|
解决方法:[点击查看如何从网盘手动下载模型](#%E5%AD%97%E5%B9%95%E7%94%9F%E6%88%90-)
|
||||||
|
|
||||||
```shell
|
|
||||||
pip uninstall Pillow
|
|
||||||
pip install Pillow==9.5.0
|
|
||||||
# 或者降级到 8.4.0
|
|
||||||
pip install Pillow==8.4.0
|
|
||||||
```
|
|
||||||
|
|
||||||
## 反馈建议 📢
|
## 反馈建议 📢
|
||||||
|
|
||||||
- 可以提交 [issue](https://github.com/harry0703/MoneyPrinterTurbo/issues)
|
- 可以提交 [issue](https://github.com/harry0703/MoneyPrinterTurbo/issues)
|
||||||
或者 [pull request](https://github.com/harry0703/MoneyPrinterTurbo/pulls)。
|
或者 [pull request](https://github.com/harry0703/MoneyPrinterTurbo/pulls)。
|
||||||
- 也可以关注我的 **抖音** 或 **视频号**:`网旭哈瑞.AI`
|
|
||||||
- 我会在上面发布一些 **使用教程** 和 **纯技术** 分享。
|
|
||||||
- 如果有更新和优化,我也会在上面 **及时通知**。
|
|
||||||
- 有问题也可以在上面 **留言**,我会 **尽快回复**。
|
|
||||||
|
|
||||||
| 抖音 | | 视频号 |
|
|
||||||
|:---------------------------------------:|:------------:|:-------------------------------------------:|
|
|
||||||
| <img src="docs/douyin.jpg" width="180"> | | <img src="docs/shipinghao.jpg" width="200"> |
|
|
||||||
|
|
||||||
## 参考项目 📚
|
## 参考项目 📚
|
||||||
|
|
||||||
@@ -393,7 +396,6 @@ pip install Pillow==8.4.0
|
|||||||
|
|
||||||
点击查看 [`LICENSE`](LICENSE) 文件
|
点击查看 [`LICENSE`](LICENSE) 文件
|
||||||
|
|
||||||
|
|
||||||
## Star History
|
## Star History
|
||||||
|
|
||||||
[](https://star-history.com/#harry0703/MoneyPrinterTurbo&Date)
|
[](https://star-history.com/#harry0703/MoneyPrinterTurbo&Date)
|
||||||
@@ -34,15 +34,16 @@ def load_config():
|
|||||||
def save_config():
|
def save_config():
|
||||||
with open(config_file, "w", encoding="utf-8") as f:
|
with open(config_file, "w", encoding="utf-8") as f:
|
||||||
_cfg["app"] = app
|
_cfg["app"] = app
|
||||||
_cfg["whisper"] = whisper
|
_cfg["azure"] = azure
|
||||||
_cfg["pexels"] = pexels
|
_cfg["ui"] = ui
|
||||||
f.write(toml.dumps(_cfg))
|
f.write(toml.dumps(_cfg))
|
||||||
|
|
||||||
|
|
||||||
_cfg = load_config()
|
_cfg = load_config()
|
||||||
app = _cfg.get("app", {})
|
app = _cfg.get("app", {})
|
||||||
whisper = _cfg.get("whisper", {})
|
whisper = _cfg.get("whisper", {})
|
||||||
pexels = _cfg.get("pexels", {})
|
proxy = _cfg.get("proxy", {})
|
||||||
|
azure = _cfg.get("azure", {})
|
||||||
ui = _cfg.get("ui", {})
|
ui = _cfg.get("ui", {})
|
||||||
|
|
||||||
hostname = socket.gethostname()
|
hostname = socket.gethostname()
|
||||||
@@ -53,7 +54,7 @@ listen_port = _cfg.get("listen_port", 8080)
|
|||||||
project_name = _cfg.get("project_name", "MoneyPrinterTurbo")
|
project_name = _cfg.get("project_name", "MoneyPrinterTurbo")
|
||||||
project_description = _cfg.get("project_description",
|
project_description = _cfg.get("project_description",
|
||||||
"<a href='https://github.com/harry0703/MoneyPrinterTurbo'>https://github.com/harry0703/MoneyPrinterTurbo</a>")
|
"<a href='https://github.com/harry0703/MoneyPrinterTurbo'>https://github.com/harry0703/MoneyPrinterTurbo</a>")
|
||||||
project_version = _cfg.get("project_version", "1.1.0")
|
project_version = _cfg.get("project_version", "1.1.9")
|
||||||
reload_debug = False
|
reload_debug = False
|
||||||
|
|
||||||
imagemagick_path = app.get("imagemagick_path", "")
|
imagemagick_path = app.get("imagemagick_path", "")
|
||||||
@@ -63,3 +64,5 @@ if imagemagick_path and os.path.isfile(imagemagick_path):
|
|||||||
ffmpeg_path = app.get("ffmpeg_path", "")
|
ffmpeg_path = app.get("ffmpeg_path", "")
|
||||||
if ffmpeg_path and os.path.isfile(ffmpeg_path):
|
if ffmpeg_path and os.path.isfile(ffmpeg_path):
|
||||||
os.environ["IMAGEIO_FFMPEG_EXE"] = ffmpeg_path
|
os.environ["IMAGEIO_FFMPEG_EXE"] = ffmpeg_path
|
||||||
|
|
||||||
|
logger.info(f"{project_name} v{project_version}")
|
||||||
|
|||||||
57
app/controllers/manager/base_manager.py
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
import threading
|
||||||
|
from typing import Callable, Any, Dict
|
||||||
|
|
||||||
|
|
||||||
|
class TaskManager:
|
||||||
|
def __init__(self, max_concurrent_tasks: int):
|
||||||
|
self.max_concurrent_tasks = max_concurrent_tasks
|
||||||
|
self.current_tasks = 0
|
||||||
|
self.lock = threading.Lock()
|
||||||
|
self.queue = self.create_queue()
|
||||||
|
|
||||||
|
def create_queue(self):
|
||||||
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
def add_task(self, func: Callable, *args: Any, **kwargs: Any):
|
||||||
|
with self.lock:
|
||||||
|
if self.current_tasks < self.max_concurrent_tasks:
|
||||||
|
print(f"add task: {func.__name__}, current_tasks: {self.current_tasks}")
|
||||||
|
self.execute_task(func, *args, **kwargs)
|
||||||
|
else:
|
||||||
|
print(f"enqueue task: {func.__name__}, current_tasks: {self.current_tasks}")
|
||||||
|
self.enqueue({"func": func, "args": args, "kwargs": kwargs})
|
||||||
|
|
||||||
|
def execute_task(self, func: Callable, *args: Any, **kwargs: Any):
|
||||||
|
thread = threading.Thread(target=self.run_task, args=(func, *args), kwargs=kwargs)
|
||||||
|
thread.start()
|
||||||
|
|
||||||
|
def run_task(self, func: Callable, *args: Any, **kwargs: Any):
|
||||||
|
try:
|
||||||
|
with self.lock:
|
||||||
|
self.current_tasks += 1
|
||||||
|
func(*args, **kwargs) # 在这里调用函数,传递*args和**kwargs
|
||||||
|
finally:
|
||||||
|
self.task_done()
|
||||||
|
|
||||||
|
def check_queue(self):
|
||||||
|
with self.lock:
|
||||||
|
if self.current_tasks < self.max_concurrent_tasks and not self.is_queue_empty():
|
||||||
|
task_info = self.dequeue()
|
||||||
|
func = task_info['func']
|
||||||
|
args = task_info.get('args', ())
|
||||||
|
kwargs = task_info.get('kwargs', {})
|
||||||
|
self.execute_task(func, *args, **kwargs)
|
||||||
|
|
||||||
|
def task_done(self):
|
||||||
|
with self.lock:
|
||||||
|
self.current_tasks -= 1
|
||||||
|
self.check_queue()
|
||||||
|
|
||||||
|
def enqueue(self, task: Dict):
|
||||||
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
def dequeue(self):
|
||||||
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
def is_queue_empty(self):
|
||||||
|
raise NotImplementedError()
|
||||||
18
app/controllers/manager/memory_manager.py
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
from queue import Queue
|
||||||
|
from typing import Dict
|
||||||
|
|
||||||
|
from app.controllers.manager.base_manager import TaskManager
|
||||||
|
|
||||||
|
|
||||||
|
class InMemoryTaskManager(TaskManager):
|
||||||
|
def create_queue(self):
|
||||||
|
return Queue()
|
||||||
|
|
||||||
|
def enqueue(self, task: Dict):
|
||||||
|
self.queue.put(task)
|
||||||
|
|
||||||
|
def dequeue(self):
|
||||||
|
return self.queue.get()
|
||||||
|
|
||||||
|
def is_queue_empty(self):
|
||||||
|
return self.queue.empty()
|
||||||
48
app/controllers/manager/redis_manager.py
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
import json
|
||||||
|
from typing import Dict
|
||||||
|
|
||||||
|
import redis
|
||||||
|
|
||||||
|
from app.controllers.manager.base_manager import TaskManager
|
||||||
|
from app.models.schema import VideoParams
|
||||||
|
from app.services import task as tm
|
||||||
|
|
||||||
|
FUNC_MAP = {
|
||||||
|
'start': tm.start,
|
||||||
|
# 'start_test': tm.start_test
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class RedisTaskManager(TaskManager):
|
||||||
|
def __init__(self, max_concurrent_tasks: int, redis_url: str):
|
||||||
|
self.redis_client = redis.Redis.from_url(redis_url)
|
||||||
|
super().__init__(max_concurrent_tasks)
|
||||||
|
|
||||||
|
def create_queue(self):
|
||||||
|
return "task_queue"
|
||||||
|
|
||||||
|
def enqueue(self, task: Dict):
|
||||||
|
task_with_serializable_params = task.copy()
|
||||||
|
|
||||||
|
if 'params' in task['kwargs'] and isinstance(task['kwargs']['params'], VideoParams):
|
||||||
|
task_with_serializable_params['kwargs']['params'] = task['kwargs']['params'].dict()
|
||||||
|
|
||||||
|
# 将函数对象转换为其名称
|
||||||
|
task_with_serializable_params['func'] = task['func'].__name__
|
||||||
|
self.redis_client.rpush(self.queue, json.dumps(task_with_serializable_params))
|
||||||
|
|
||||||
|
def dequeue(self):
|
||||||
|
task_json = self.redis_client.lpop(self.queue)
|
||||||
|
if task_json:
|
||||||
|
task_info = json.loads(task_json)
|
||||||
|
# 将函数名称转换回函数对象
|
||||||
|
task_info['func'] = FUNC_MAP[task_info['func']]
|
||||||
|
|
||||||
|
if 'params' in task_info['kwargs'] and isinstance(task_info['kwargs']['params'], dict):
|
||||||
|
task_info['kwargs']['params'] = VideoParams(**task_info['kwargs']['params'])
|
||||||
|
|
||||||
|
return task_info
|
||||||
|
return None
|
||||||
|
|
||||||
|
def is_queue_empty(self):
|
||||||
|
return self.redis_client.llen(self.queue) == 0
|
||||||
@@ -1,13 +1,17 @@
|
|||||||
import os
|
import os
|
||||||
import glob
|
import glob
|
||||||
|
import pathlib
|
||||||
import shutil
|
import shutil
|
||||||
|
|
||||||
from fastapi import Request, Depends, Path, BackgroundTasks, UploadFile
|
from fastapi import Request, Depends, Path, BackgroundTasks, UploadFile
|
||||||
|
from fastapi.responses import FileResponse, StreamingResponse
|
||||||
from fastapi.params import File
|
from fastapi.params import File
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
|
|
||||||
from app.config import config
|
from app.config import config
|
||||||
from app.controllers import base
|
from app.controllers import base
|
||||||
|
from app.controllers.manager.memory_manager import InMemoryTaskManager
|
||||||
|
from app.controllers.manager.redis_manager import RedisTaskManager
|
||||||
from app.controllers.v1.base import new_router
|
from app.controllers.v1.base import new_router
|
||||||
from app.models.exception import HttpException
|
from app.models.exception import HttpException
|
||||||
from app.models.schema import TaskVideoRequest, TaskQueryResponse, TaskResponse, TaskQueryRequest, \
|
from app.models.schema import TaskVideoRequest, TaskQueryResponse, TaskResponse, TaskQueryRequest, \
|
||||||
@@ -20,6 +24,35 @@ from app.utils import utils
|
|||||||
# router = new_router(dependencies=[Depends(base.verify_token)])
|
# router = new_router(dependencies=[Depends(base.verify_token)])
|
||||||
router = new_router()
|
router = new_router()
|
||||||
|
|
||||||
|
_enable_redis = config.app.get("enable_redis", False)
|
||||||
|
_redis_host = config.app.get("redis_host", "localhost")
|
||||||
|
_redis_port = config.app.get("redis_port", 6379)
|
||||||
|
_redis_db = config.app.get("redis_db", 0)
|
||||||
|
_redis_password = config.app.get("redis_password", None)
|
||||||
|
_max_concurrent_tasks = config.app.get("max_concurrent_tasks", 5)
|
||||||
|
|
||||||
|
redis_url = f"redis://:{_redis_password}@{_redis_host}:{_redis_port}/{_redis_db}"
|
||||||
|
# 根据配置选择合适的任务管理器
|
||||||
|
if _enable_redis:
|
||||||
|
task_manager = RedisTaskManager(max_concurrent_tasks=_max_concurrent_tasks, redis_url=redis_url)
|
||||||
|
else:
|
||||||
|
task_manager = InMemoryTaskManager(max_concurrent_tasks=_max_concurrent_tasks)
|
||||||
|
|
||||||
|
# @router.post("/videos-test", response_model=TaskResponse, summary="Generate a short video")
|
||||||
|
# async def create_video_test(request: Request, body: TaskVideoRequest):
|
||||||
|
# task_id = utils.get_uuid()
|
||||||
|
# request_id = base.get_task_id(request)
|
||||||
|
# try:
|
||||||
|
# task = {
|
||||||
|
# "task_id": task_id,
|
||||||
|
# "request_id": request_id,
|
||||||
|
# "params": body.dict(),
|
||||||
|
# }
|
||||||
|
# task_manager.add_task(tm.start_test, task_id=task_id, params=body)
|
||||||
|
# return utils.get_response(200, task)
|
||||||
|
# except ValueError as e:
|
||||||
|
# raise HttpException(task_id=task_id, status_code=400, message=f"{request_id}: {str(e)}")
|
||||||
|
|
||||||
|
|
||||||
@router.post("/videos", response_model=TaskResponse, summary="Generate a short video")
|
@router.post("/videos", response_model=TaskResponse, summary="Generate a short video")
|
||||||
def create_video(background_tasks: BackgroundTasks, request: Request, body: TaskVideoRequest):
|
def create_video(background_tasks: BackgroundTasks, request: Request, body: TaskVideoRequest):
|
||||||
@@ -32,7 +65,8 @@ def create_video(background_tasks: BackgroundTasks, request: Request, body: Task
|
|||||||
"params": body.dict(),
|
"params": body.dict(),
|
||||||
}
|
}
|
||||||
sm.state.update_task(task_id)
|
sm.state.update_task(task_id)
|
||||||
background_tasks.add_task(tm.start, task_id=task_id, params=body)
|
# background_tasks.add_task(tm.start, task_id=task_id, params=body)
|
||||||
|
task_manager.add_task(tm.start, task_id=task_id, params=body)
|
||||||
logger.success(f"video created: {utils.to_json(task)}")
|
logger.success(f"video created: {utils.to_json(task)}")
|
||||||
return utils.get_response(200, task)
|
return utils.get_response(200, task)
|
||||||
except ValueError as e:
|
except ValueError as e:
|
||||||
@@ -78,7 +112,7 @@ def get_task(request: Request, task_id: str = Path(..., description="Task ID"),
|
|||||||
|
|
||||||
|
|
||||||
@router.delete("/tasks/{task_id}", response_model=TaskDeletionResponse, summary="Delete a generated short video task")
|
@router.delete("/tasks/{task_id}", response_model=TaskDeletionResponse, summary="Delete a generated short video task")
|
||||||
def create_video(request: Request, task_id: str = Path(..., description="Task ID")):
|
def delete_video(request: Request, task_id: str = Path(..., description="Task ID")):
|
||||||
request_id = base.get_task_id(request)
|
request_id = base.get_task_id(request)
|
||||||
task = sm.state.get_task(task_id)
|
task = sm.state.get_task(task_id)
|
||||||
if task:
|
if task:
|
||||||
@@ -89,7 +123,7 @@ def create_video(request: Request, task_id: str = Path(..., description="Task ID
|
|||||||
|
|
||||||
sm.state.delete_task(task_id)
|
sm.state.delete_task(task_id)
|
||||||
logger.success(f"video deleted: {utils.to_json(task)}")
|
logger.success(f"video deleted: {utils.to_json(task)}")
|
||||||
return utils.get_response(200, task)
|
return utils.get_response(200)
|
||||||
|
|
||||||
raise HttpException(task_id=task_id, status_code=404, message=f"{request_id}: task not found")
|
raise HttpException(task_id=task_id, status_code=404, message=f"{request_id}: task not found")
|
||||||
|
|
||||||
@@ -130,3 +164,63 @@ def upload_bgm_file(request: Request, file: UploadFile = File(...)):
|
|||||||
return utils.get_response(200, response)
|
return utils.get_response(200, response)
|
||||||
|
|
||||||
raise HttpException('', status_code=400, message=f"{request_id}: Only *.mp3 files can be uploaded")
|
raise HttpException('', status_code=400, message=f"{request_id}: Only *.mp3 files can be uploaded")
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/stream/{file_path:path}")
|
||||||
|
async def stream_video(request: Request, file_path: str):
|
||||||
|
tasks_dir = utils.task_dir()
|
||||||
|
video_path = os.path.join(tasks_dir, file_path)
|
||||||
|
range_header = request.headers.get('Range')
|
||||||
|
video_size = os.path.getsize(video_path)
|
||||||
|
start, end = 0, video_size - 1
|
||||||
|
|
||||||
|
length = video_size
|
||||||
|
if range_header:
|
||||||
|
range_ = range_header.split('bytes=')[1]
|
||||||
|
start, end = [int(part) if part else None for part in range_.split('-')]
|
||||||
|
if start is None:
|
||||||
|
start = video_size - end
|
||||||
|
end = video_size - 1
|
||||||
|
if end is None:
|
||||||
|
end = video_size - 1
|
||||||
|
length = end - start + 1
|
||||||
|
|
||||||
|
def file_iterator(file_path, offset=0, bytes_to_read=None):
|
||||||
|
with open(file_path, 'rb') as f:
|
||||||
|
f.seek(offset, os.SEEK_SET)
|
||||||
|
remaining = bytes_to_read or video_size
|
||||||
|
while remaining > 0:
|
||||||
|
bytes_to_read = min(4096, remaining)
|
||||||
|
data = f.read(bytes_to_read)
|
||||||
|
if not data:
|
||||||
|
break
|
||||||
|
remaining -= len(data)
|
||||||
|
yield data
|
||||||
|
|
||||||
|
response = StreamingResponse(file_iterator(video_path, start, length), media_type='video/mp4')
|
||||||
|
response.headers['Content-Range'] = f'bytes {start}-{end}/{video_size}'
|
||||||
|
response.headers['Accept-Ranges'] = 'bytes'
|
||||||
|
response.headers['Content-Length'] = str(length)
|
||||||
|
response.status_code = 206 # Partial Content
|
||||||
|
|
||||||
|
return response
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/download/{file_path:path}")
|
||||||
|
async def download_video(_: Request, file_path: str):
|
||||||
|
"""
|
||||||
|
download video
|
||||||
|
:param _: Request request
|
||||||
|
:param file_path: video file path, eg: /cd1727ed-3473-42a2-a7da-4faafafec72b/final-1.mp4
|
||||||
|
:return: video file
|
||||||
|
"""
|
||||||
|
tasks_dir = utils.task_dir()
|
||||||
|
video_path = os.path.join(tasks_dir, file_path)
|
||||||
|
file_path = pathlib.Path(video_path)
|
||||||
|
filename = file_path.stem
|
||||||
|
extension = file_path.suffix
|
||||||
|
headers = {
|
||||||
|
"Content-Disposition": f"attachment; filename={filename}{extension}"
|
||||||
|
}
|
||||||
|
return FileResponse(path=video_path, headers=headers, filename=f"{filename}{extension}",
|
||||||
|
media_type=f'video/{extension[1:]}')
|
||||||
|
|||||||
@@ -6,3 +6,6 @@ PUNCTUATIONS = [
|
|||||||
TASK_STATE_FAILED = -1
|
TASK_STATE_FAILED = -1
|
||||||
TASK_STATE_COMPLETE = 1
|
TASK_STATE_COMPLETE = 1
|
||||||
TASK_STATE_PROCESSING = 4
|
TASK_STATE_PROCESSING = 4
|
||||||
|
|
||||||
|
FILE_TYPE_VIDEOS = ['mp4', 'mov', 'mkv', 'webm']
|
||||||
|
FILE_TYPE_IMAGES = ['jpg', 'jpeg', 'png', 'bmp']
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
from enum import Enum
|
from enum import Enum
|
||||||
from typing import Any, Optional
|
from typing import Any, Optional, List
|
||||||
|
|
||||||
|
import pydantic
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
import warnings
|
import warnings
|
||||||
|
|
||||||
@@ -28,6 +29,11 @@ class VideoAspect(str, Enum):
|
|||||||
return 1080, 1920
|
return 1080, 1920
|
||||||
|
|
||||||
|
|
||||||
|
class _Config:
|
||||||
|
arbitrary_types_allowed = True
|
||||||
|
|
||||||
|
|
||||||
|
@pydantic.dataclasses.dataclass(config=_Config)
|
||||||
class MaterialInfo:
|
class MaterialInfo:
|
||||||
provider: str = "pexels"
|
provider: str = "pexels"
|
||||||
url: str = ""
|
url: str = ""
|
||||||
@@ -73,7 +79,7 @@ class MaterialInfo:
|
|||||||
# ]
|
# ]
|
||||||
|
|
||||||
|
|
||||||
class VideoParams:
|
class VideoParams(BaseModel):
|
||||||
"""
|
"""
|
||||||
{
|
{
|
||||||
"video_subject": "",
|
"video_subject": "",
|
||||||
@@ -95,6 +101,9 @@ class VideoParams:
|
|||||||
video_clip_duration: Optional[int] = 5
|
video_clip_duration: Optional[int] = 5
|
||||||
video_count: Optional[int] = 1
|
video_count: Optional[int] = 1
|
||||||
|
|
||||||
|
video_source: Optional[str] = "pexels"
|
||||||
|
video_materials: Optional[List[MaterialInfo]] = None # 用于生成视频的素材
|
||||||
|
|
||||||
video_language: Optional[str] = "" # auto detect
|
video_language: Optional[str] = "" # auto detect
|
||||||
|
|
||||||
voice_name: Optional[str] = ""
|
voice_name: Optional[str] = ""
|
||||||
|
|||||||
@@ -9,6 +9,8 @@ from openai.types.chat import ChatCompletion
|
|||||||
|
|
||||||
from app.config import config
|
from app.config import config
|
||||||
|
|
||||||
|
_max_retries = 5
|
||||||
|
|
||||||
|
|
||||||
def _generate_response(prompt: str) -> str:
|
def _generate_response(prompt: str) -> str:
|
||||||
content = ""
|
content = ""
|
||||||
@@ -59,6 +61,17 @@ def _generate_response(prompt: str) -> str:
|
|||||||
api_key = config.app.get("qwen_api_key")
|
api_key = config.app.get("qwen_api_key")
|
||||||
model_name = config.app.get("qwen_model_name")
|
model_name = config.app.get("qwen_model_name")
|
||||||
base_url = "***"
|
base_url = "***"
|
||||||
|
elif llm_provider == "cloudflare":
|
||||||
|
api_key = config.app.get("cloudflare_api_key")
|
||||||
|
model_name = config.app.get("cloudflare_model_name")
|
||||||
|
account_id = config.app.get("cloudflare_account_id")
|
||||||
|
base_url = "***"
|
||||||
|
elif llm_provider == "deepseek":
|
||||||
|
api_key = config.app.get("deepseek_api_key")
|
||||||
|
model_name = config.app.get("deepseek_model_name")
|
||||||
|
base_url = config.app.get("deepseek_base_url")
|
||||||
|
if not base_url:
|
||||||
|
base_url = "https://api.deepseek.com"
|
||||||
else:
|
else:
|
||||||
raise ValueError("llm_provider is not set, please set it in the config.toml file.")
|
raise ValueError("llm_provider is not set, please set it in the config.toml file.")
|
||||||
|
|
||||||
@@ -71,17 +84,31 @@ def _generate_response(prompt: str) -> str:
|
|||||||
|
|
||||||
if llm_provider == "qwen":
|
if llm_provider == "qwen":
|
||||||
import dashscope
|
import dashscope
|
||||||
|
from dashscope.api_entities.dashscope_response import GenerationResponse
|
||||||
dashscope.api_key = api_key
|
dashscope.api_key = api_key
|
||||||
response = dashscope.Generation.call(
|
response = dashscope.Generation.call(
|
||||||
model=model_name,
|
model=model_name,
|
||||||
messages=[{"role": "user", "content": prompt}]
|
messages=[{"role": "user", "content": prompt}]
|
||||||
)
|
)
|
||||||
|
if response:
|
||||||
|
if isinstance(response, GenerationResponse):
|
||||||
|
status_code = response.status_code
|
||||||
|
if status_code != 200:
|
||||||
|
raise Exception(
|
||||||
|
f"[{llm_provider}] returned an error response: \"{response}\"")
|
||||||
|
|
||||||
content = response["output"]["text"]
|
content = response["output"]["text"]
|
||||||
return content.replace("\n", "")
|
return content.replace("\n", "")
|
||||||
|
else:
|
||||||
|
raise Exception(
|
||||||
|
f"[{llm_provider}] returned an invalid response: \"{response}\"")
|
||||||
|
else:
|
||||||
|
raise Exception(
|
||||||
|
f"[{llm_provider}] returned an empty response")
|
||||||
|
|
||||||
if llm_provider == "gemini":
|
if llm_provider == "gemini":
|
||||||
import google.generativeai as genai
|
import google.generativeai as genai
|
||||||
genai.configure(api_key=api_key)
|
genai.configure(api_key=api_key, transport='rest')
|
||||||
|
|
||||||
generation_config = {
|
generation_config = {
|
||||||
"temperature": 0.5,
|
"temperature": 0.5,
|
||||||
@@ -113,10 +140,30 @@ def _generate_response(prompt: str) -> str:
|
|||||||
generation_config=generation_config,
|
generation_config=generation_config,
|
||||||
safety_settings=safety_settings)
|
safety_settings=safety_settings)
|
||||||
|
|
||||||
convo = model.start_chat(history=[])
|
try:
|
||||||
|
response = model.generate_content(prompt)
|
||||||
|
candidates = response.candidates
|
||||||
|
generated_text = candidates[0].content.parts[0].text
|
||||||
|
except (AttributeError, IndexError) as e:
|
||||||
|
print("Gemini Error:", e)
|
||||||
|
|
||||||
convo.send_message(prompt)
|
return generated_text
|
||||||
return convo.last.text
|
|
||||||
|
if llm_provider == "cloudflare":
|
||||||
|
import requests
|
||||||
|
response = requests.post(
|
||||||
|
f"https://api.cloudflare.com/client/v4/accounts/{account_id}/ai/run/{model_name}",
|
||||||
|
headers={"Authorization": f"Bearer {api_key}"},
|
||||||
|
json={
|
||||||
|
"messages": [
|
||||||
|
{"role": "system", "content": "You are a friendly assistant"},
|
||||||
|
{"role": "user", "content": prompt}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
)
|
||||||
|
result = response.json()
|
||||||
|
logger.info(result)
|
||||||
|
return result["result"]["response"]
|
||||||
|
|
||||||
if llm_provider == "azure":
|
if llm_provider == "azure":
|
||||||
client = AzureOpenAI(
|
client = AzureOpenAI(
|
||||||
@@ -174,11 +221,8 @@ Generate a script for a video, depending on the subject of the video.
|
|||||||
|
|
||||||
final_script = ""
|
final_script = ""
|
||||||
logger.info(f"subject: {video_subject}")
|
logger.info(f"subject: {video_subject}")
|
||||||
logger.debug(f"prompt: \n{prompt}")
|
|
||||||
response = _generate_response(prompt=prompt)
|
|
||||||
|
|
||||||
# Return the generated script
|
def format_response(response):
|
||||||
if response:
|
|
||||||
# Clean the script
|
# Clean the script
|
||||||
# Remove asterisks, hashes
|
# Remove asterisks, hashes
|
||||||
response = response.replace("*", "")
|
response = response.replace("*", "")
|
||||||
@@ -195,15 +239,30 @@ Generate a script for a video, depending on the subject of the video.
|
|||||||
selected_paragraphs = paragraphs[:paragraph_number]
|
selected_paragraphs = paragraphs[:paragraph_number]
|
||||||
|
|
||||||
# Join the selected paragraphs into a single string
|
# Join the selected paragraphs into a single string
|
||||||
final_script = "\n\n".join(selected_paragraphs)
|
return "\n\n".join(selected_paragraphs)
|
||||||
|
|
||||||
# Print to console the number of paragraphs used
|
for i in range(_max_retries):
|
||||||
# logger.info(f"number of paragraphs used: {len(selected_paragraphs)}")
|
try:
|
||||||
|
response = _generate_response(prompt=prompt)
|
||||||
|
if response:
|
||||||
|
final_script = format_response(response)
|
||||||
else:
|
else:
|
||||||
logging.error("gpt returned an empty response")
|
logging.error("gpt returned an empty response")
|
||||||
|
|
||||||
|
# g4f may return an error message
|
||||||
|
if final_script and "当日额度已消耗完" in final_script:
|
||||||
|
raise ValueError(final_script)
|
||||||
|
|
||||||
|
if final_script:
|
||||||
|
break
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"failed to generate script: {e}")
|
||||||
|
|
||||||
|
if i < _max_retries:
|
||||||
|
logger.warning(f"failed to generate video script, trying again... {i + 1}")
|
||||||
|
|
||||||
logger.success(f"completed: \n{final_script}")
|
logger.success(f"completed: \n{final_script}")
|
||||||
return final_script
|
return final_script.strip()
|
||||||
|
|
||||||
|
|
||||||
def generate_terms(video_subject: str, video_script: str, amount: int = 5) -> List[str]:
|
def generate_terms(video_subject: str, video_script: str, amount: int = 5) -> List[str]:
|
||||||
@@ -234,25 +293,28 @@ Please note that you must use English for generating video search terms; Chinese
|
|||||||
""".strip()
|
""".strip()
|
||||||
|
|
||||||
logger.info(f"subject: {video_subject}")
|
logger.info(f"subject: {video_subject}")
|
||||||
logger.debug(f"prompt: \n{prompt}")
|
|
||||||
response = _generate_response(prompt)
|
|
||||||
search_terms = []
|
|
||||||
|
|
||||||
|
search_terms = []
|
||||||
|
for i in range(_max_retries):
|
||||||
try:
|
try:
|
||||||
|
response = _generate_response(prompt)
|
||||||
search_terms = json.loads(response)
|
search_terms = json.loads(response)
|
||||||
if not isinstance(search_terms, list) or not all(isinstance(term, str) for term in search_terms):
|
if not isinstance(search_terms, list) or not all(isinstance(term, str) for term in search_terms):
|
||||||
raise ValueError("response is not a list of strings.")
|
logger.error("response is not a list of strings.")
|
||||||
|
continue
|
||||||
|
|
||||||
except (json.JSONDecodeError, ValueError):
|
except Exception as e:
|
||||||
# logger.warning(f"gpt returned an unformatted response. attempting to clean...")
|
match = re.search(r'\[.*]', response)
|
||||||
# Attempt to extract list-like string and convert to list
|
|
||||||
match = re.search(r'\["(?:[^"\\]|\\.)*"(?:,\s*"[^"\\]*")*\]', response)
|
|
||||||
if match:
|
if match:
|
||||||
try:
|
try:
|
||||||
search_terms = json.loads(match.group())
|
search_terms = json.loads(match.group())
|
||||||
except json.JSONDecodeError:
|
except json.JSONDecodeError:
|
||||||
logger.error(f"could not parse response: {response}")
|
pass
|
||||||
return []
|
|
||||||
|
if search_terms and len(search_terms) > 0:
|
||||||
|
break
|
||||||
|
if i < _max_retries:
|
||||||
|
logger.warning(f"failed to generate video terms, trying again... {i + 1}")
|
||||||
|
|
||||||
logger.success(f"completed: \n{search_terms}")
|
logger.success(f"completed: \n{search_terms}")
|
||||||
return search_terms
|
return search_terms
|
||||||
@@ -261,8 +323,8 @@ Please note that you must use English for generating video search terms; Chinese
|
|||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
video_subject = "生命的意义是什么"
|
video_subject = "生命的意义是什么"
|
||||||
script = generate_script(video_subject=video_subject, language="zh-CN", paragraph_number=1)
|
script = generate_script(video_subject=video_subject, language="zh-CN", paragraph_number=1)
|
||||||
# print("######################")
|
print("######################")
|
||||||
# print(script)
|
print(script)
|
||||||
# search_terms = generate_terms(video_subject=video_subject, video_script=script, amount=5)
|
search_terms = generate_terms(video_subject=video_subject, video_script=script, amount=5)
|
||||||
# print("######################")
|
print("######################")
|
||||||
# print(search_terms)
|
print(search_terms)
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ from urllib.parse import urlencode
|
|||||||
import requests
|
import requests
|
||||||
from typing import List
|
from typing import List
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
|
from moviepy.video.io.VideoFileClip import VideoFileClip
|
||||||
|
|
||||||
from app.config import config
|
from app.config import config
|
||||||
from app.models.schema import VideoAspect, VideoConcatMode, MaterialInfo
|
from app.models.schema import VideoAspect, VideoConcatMode, MaterialInfo
|
||||||
@@ -13,33 +14,33 @@ from app.utils import utils
|
|||||||
requested_count = 0
|
requested_count = 0
|
||||||
|
|
||||||
|
|
||||||
def round_robin_api_key():
|
def get_api_key(cfg_key: str):
|
||||||
pexels_api_keys = config.app.get("pexels_api_keys")
|
api_keys = config.app.get(cfg_key)
|
||||||
if not pexels_api_keys:
|
if not api_keys:
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
f"\n\n##### pexels_api_keys is not set #####\n\nPlease set it in the config.toml file: {config.config_file}\n\n{utils.to_json(config.app)}")
|
f"\n\n##### {cfg_key} is not set #####\n\nPlease set it in the config.toml file: {config.config_file}\n\n"
|
||||||
|
f"{utils.to_json(config.app)}")
|
||||||
|
|
||||||
# if only one key is provided, return it
|
# if only one key is provided, return it
|
||||||
if isinstance(pexels_api_keys, str):
|
if isinstance(api_keys, str):
|
||||||
return pexels_api_keys
|
return api_keys
|
||||||
|
|
||||||
global requested_count
|
global requested_count
|
||||||
requested_count += 1
|
requested_count += 1
|
||||||
return pexels_api_keys[requested_count % len(pexels_api_keys)]
|
return api_keys[requested_count % len(api_keys)]
|
||||||
|
|
||||||
|
|
||||||
def search_videos(search_term: str,
|
def search_videos_pexels(search_term: str,
|
||||||
minimum_duration: int,
|
minimum_duration: int,
|
||||||
video_aspect: VideoAspect = VideoAspect.portrait,
|
video_aspect: VideoAspect = VideoAspect.portrait,
|
||||||
) -> List[MaterialInfo]:
|
) -> List[MaterialInfo]:
|
||||||
aspect = VideoAspect(video_aspect)
|
aspect = VideoAspect(video_aspect)
|
||||||
video_orientation = aspect.name
|
video_orientation = aspect.name
|
||||||
video_width, video_height = aspect.to_resolution()
|
video_width, video_height = aspect.to_resolution()
|
||||||
|
api_key = get_api_key("pexels_api_keys")
|
||||||
headers = {
|
headers = {
|
||||||
"Authorization": round_robin_api_key()
|
"Authorization": api_key
|
||||||
}
|
}
|
||||||
proxies = config.pexels.get("proxies", None)
|
|
||||||
# Build URL
|
# Build URL
|
||||||
params = {
|
params = {
|
||||||
"query": search_term,
|
"query": search_term,
|
||||||
@@ -47,10 +48,10 @@ def search_videos(search_term: str,
|
|||||||
"orientation": video_orientation
|
"orientation": video_orientation
|
||||||
}
|
}
|
||||||
query_url = f"https://api.pexels.com/videos/search?{urlencode(params)}"
|
query_url = f"https://api.pexels.com/videos/search?{urlencode(params)}"
|
||||||
logger.info(f"searching videos: {query_url}, with proxies: {proxies}")
|
logger.info(f"searching videos: {query_url}, with proxies: {config.proxy}")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
r = requests.get(query_url, headers=headers, proxies=proxies, verify=False)
|
r = requests.get(query_url, headers=headers, proxies=config.proxy, verify=False, timeout=(30, 60))
|
||||||
response = r.json()
|
response = r.json()
|
||||||
video_items = []
|
video_items = []
|
||||||
if "videos" not in response:
|
if "videos" not in response:
|
||||||
@@ -82,6 +83,59 @@ def search_videos(search_term: str,
|
|||||||
return []
|
return []
|
||||||
|
|
||||||
|
|
||||||
|
def search_videos_pixabay(search_term: str,
|
||||||
|
minimum_duration: int,
|
||||||
|
video_aspect: VideoAspect = VideoAspect.portrait,
|
||||||
|
) -> List[MaterialInfo]:
|
||||||
|
aspect = VideoAspect(video_aspect)
|
||||||
|
|
||||||
|
video_width, video_height = aspect.to_resolution()
|
||||||
|
|
||||||
|
api_key = get_api_key("pixabay_api_keys")
|
||||||
|
# Build URL
|
||||||
|
params = {
|
||||||
|
"q": search_term,
|
||||||
|
"video_type": "all", # Accepted values: "all", "film", "animation"
|
||||||
|
"per_page": 50,
|
||||||
|
"key": api_key
|
||||||
|
}
|
||||||
|
query_url = f"https://pixabay.com/api/videos/?{urlencode(params)}"
|
||||||
|
logger.info(f"searching videos: {query_url}, with proxies: {config.proxy}")
|
||||||
|
|
||||||
|
try:
|
||||||
|
r = requests.get(query_url, proxies=config.proxy, verify=False, timeout=(30, 60))
|
||||||
|
response = r.json()
|
||||||
|
video_items = []
|
||||||
|
if "hits" not in response:
|
||||||
|
logger.error(f"search videos failed: {response}")
|
||||||
|
return video_items
|
||||||
|
videos = response["hits"]
|
||||||
|
# loop through each video in the result
|
||||||
|
for v in videos:
|
||||||
|
duration = v["duration"]
|
||||||
|
# check if video has desired minimum duration
|
||||||
|
if duration < minimum_duration:
|
||||||
|
continue
|
||||||
|
video_files = v["videos"]
|
||||||
|
# loop through each url to determine the best quality
|
||||||
|
for video_type in video_files:
|
||||||
|
video = video_files[video_type]
|
||||||
|
w = int(video["width"])
|
||||||
|
h = int(video["height"])
|
||||||
|
if w >= video_width:
|
||||||
|
item = MaterialInfo()
|
||||||
|
item.provider = "pixabay"
|
||||||
|
item.url = video["url"]
|
||||||
|
item.duration = duration
|
||||||
|
video_items.append(item)
|
||||||
|
break
|
||||||
|
return video_items
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"search videos failed: {str(e)}")
|
||||||
|
|
||||||
|
return []
|
||||||
|
|
||||||
|
|
||||||
def save_video(video_url: str, save_dir: str = "") -> str:
|
def save_video(video_url: str, save_dir: str = "") -> str:
|
||||||
if not save_dir:
|
if not save_dir:
|
||||||
save_dir = utils.storage_dir("cache_videos")
|
save_dir = utils.storage_dir("cache_videos")
|
||||||
@@ -100,17 +154,29 @@ def save_video(video_url: str, save_dir: str = "") -> str:
|
|||||||
return video_path
|
return video_path
|
||||||
|
|
||||||
# if video does not exist, download it
|
# if video does not exist, download it
|
||||||
proxies = config.pexels.get("proxies", None)
|
|
||||||
with open(video_path, "wb") as f:
|
with open(video_path, "wb") as f:
|
||||||
f.write(requests.get(video_url, proxies=proxies, verify=False, timeout=(60, 240)).content)
|
f.write(requests.get(video_url, proxies=config.proxy, verify=False, timeout=(60, 240)).content)
|
||||||
|
|
||||||
if os.path.exists(video_path) and os.path.getsize(video_path) > 0:
|
if os.path.exists(video_path) and os.path.getsize(video_path) > 0:
|
||||||
|
try:
|
||||||
|
clip = VideoFileClip(video_path)
|
||||||
|
duration = clip.duration
|
||||||
|
fps = clip.fps
|
||||||
|
clip.close()
|
||||||
|
if duration > 0 and fps > 0:
|
||||||
return video_path
|
return video_path
|
||||||
|
except Exception as e:
|
||||||
|
try:
|
||||||
|
os.remove(video_path)
|
||||||
|
except Exception as e:
|
||||||
|
pass
|
||||||
|
logger.warning(f"invalid video file: {video_path} => {str(e)}")
|
||||||
return ""
|
return ""
|
||||||
|
|
||||||
|
|
||||||
def download_videos(task_id: str,
|
def download_videos(task_id: str,
|
||||||
search_terms: List[str],
|
search_terms: List[str],
|
||||||
|
source: str = "pexels",
|
||||||
video_aspect: VideoAspect = VideoAspect.portrait,
|
video_aspect: VideoAspect = VideoAspect.portrait,
|
||||||
video_contact_mode: VideoConcatMode = VideoConcatMode.random,
|
video_contact_mode: VideoConcatMode = VideoConcatMode.random,
|
||||||
audio_duration: float = 0.0,
|
audio_duration: float = 0.0,
|
||||||
@@ -119,8 +185,11 @@ def download_videos(task_id: str,
|
|||||||
valid_video_items = []
|
valid_video_items = []
|
||||||
valid_video_urls = []
|
valid_video_urls = []
|
||||||
found_duration = 0.0
|
found_duration = 0.0
|
||||||
|
search_videos = search_videos_pexels
|
||||||
|
if source == "pixabay":
|
||||||
|
search_videos = search_videos_pixabay
|
||||||
|
|
||||||
for search_term in search_terms:
|
for search_term in search_terms:
|
||||||
# logger.info(f"searching videos for '{search_term}'")
|
|
||||||
video_items = search_videos(search_term=search_term,
|
video_items = search_videos(search_term=search_term,
|
||||||
minimum_duration=max_clip_duration,
|
minimum_duration=max_clip_duration,
|
||||||
video_aspect=video_aspect)
|
video_aspect=video_aspect)
|
||||||
@@ -165,4 +234,4 @@ def download_videos(task_id: str,
|
|||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
download_videos("test123", ["cat"], audio_duration=100)
|
download_videos("test123", ["Money Exchange Medium"], audio_duration=100, source="pixabay")
|
||||||
|
|||||||
@@ -44,9 +44,9 @@ class MemoryState(BaseState):
|
|||||||
# Redis state management
|
# Redis state management
|
||||||
class RedisState(BaseState):
|
class RedisState(BaseState):
|
||||||
|
|
||||||
def __init__(self, host='localhost', port=6379, db=0):
|
def __init__(self, host='localhost', port=6379, db=0, password=None):
|
||||||
import redis
|
import redis
|
||||||
self._redis = redis.StrictRedis(host=host, port=port, db=db)
|
self._redis = redis.StrictRedis(host=host, port=port, db=db, password=password)
|
||||||
|
|
||||||
def update_task(self, task_id: str, state: int = const.TASK_STATE_PROCESSING, progress: int = 0, **kwargs):
|
def update_task(self, task_id: str, state: int = const.TASK_STATE_PROCESSING, progress: int = 0, **kwargs):
|
||||||
progress = int(progress)
|
progress = int(progress)
|
||||||
@@ -98,5 +98,6 @@ _enable_redis = config.app.get("enable_redis", False)
|
|||||||
_redis_host = config.app.get("redis_host", "localhost")
|
_redis_host = config.app.get("redis_host", "localhost")
|
||||||
_redis_port = config.app.get("redis_port", 6379)
|
_redis_port = config.app.get("redis_port", 6379)
|
||||||
_redis_db = config.app.get("redis_db", 0)
|
_redis_db = config.app.get("redis_db", 0)
|
||||||
|
_redis_password = config.app.get("redis_password", None)
|
||||||
|
|
||||||
state = RedisState(host=_redis_host, port=_redis_port, db=_redis_db) if _enable_redis else MemoryState()
|
state = RedisState(host=_redis_host, port=_redis_port, db=_redis_db, password=_redis_password) if _enable_redis else MemoryState()
|
||||||
|
|||||||
@@ -24,9 +24,18 @@ def create(audio_file, subtitle_file: str = ""):
|
|||||||
model_path = model_size
|
model_path = model_size
|
||||||
|
|
||||||
logger.info(f"loading model: {model_path}, device: {device}, compute_type: {compute_type}")
|
logger.info(f"loading model: {model_path}, device: {device}, compute_type: {compute_type}")
|
||||||
|
try:
|
||||||
model = WhisperModel(model_size_or_path=model_path,
|
model = WhisperModel(model_size_or_path=model_path,
|
||||||
device=device,
|
device=device,
|
||||||
compute_type=compute_type)
|
compute_type=compute_type)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"failed to load model: {e} \n\n"
|
||||||
|
f"********************************************\n"
|
||||||
|
f"this may be caused by network issue. \n"
|
||||||
|
f"please download the model manually and put it in the 'models' folder. \n"
|
||||||
|
f"see [README.md FAQ](https://github.com/harry0703/MoneyPrinterTurbo) for more details.\n"
|
||||||
|
f"********************************************\n\n")
|
||||||
|
return None
|
||||||
|
|
||||||
logger.info(f"start, output file: {subtitle_file}")
|
logger.info(f"start, output file: {subtitle_file}")
|
||||||
if not subtitle_file:
|
if not subtitle_file:
|
||||||
@@ -120,6 +129,9 @@ def create(audio_file, subtitle_file: str = ""):
|
|||||||
|
|
||||||
|
|
||||||
def file_to_subtitles(filename):
|
def file_to_subtitles(filename):
|
||||||
|
if not filename or not os.path.isfile(filename):
|
||||||
|
return []
|
||||||
|
|
||||||
times_texts = []
|
times_texts = []
|
||||||
current_times = None
|
current_times = None
|
||||||
current_text = ""
|
current_text = ""
|
||||||
|
|||||||
@@ -44,6 +44,11 @@ def start(task_id, params: VideoParams):
|
|||||||
else:
|
else:
|
||||||
logger.debug(f"video script: \n{video_script}")
|
logger.debug(f"video script: \n{video_script}")
|
||||||
|
|
||||||
|
if not video_script:
|
||||||
|
sm.state.update_task(task_id, state=const.TASK_STATE_FAILED)
|
||||||
|
logger.error("failed to generate video script.")
|
||||||
|
return
|
||||||
|
|
||||||
sm.state.update_task(task_id, state=const.TASK_STATE_PROCESSING, progress=10)
|
sm.state.update_task(task_id, state=const.TASK_STATE_PROCESSING, progress=10)
|
||||||
|
|
||||||
logger.info("\n\n## generating video terms")
|
logger.info("\n\n## generating video terms")
|
||||||
@@ -60,6 +65,11 @@ def start(task_id, params: VideoParams):
|
|||||||
|
|
||||||
logger.debug(f"video terms: {utils.to_json(video_terms)}")
|
logger.debug(f"video terms: {utils.to_json(video_terms)}")
|
||||||
|
|
||||||
|
if not video_terms:
|
||||||
|
sm.state.update_task(task_id, state=const.TASK_STATE_FAILED)
|
||||||
|
logger.error("failed to generate video terms.")
|
||||||
|
return
|
||||||
|
|
||||||
script_file = path.join(utils.task_dir(task_id), f"script.json")
|
script_file = path.join(utils.task_dir(task_id), f"script.json")
|
||||||
script_data = {
|
script_data = {
|
||||||
"script": video_script,
|
"script": video_script,
|
||||||
@@ -78,7 +88,11 @@ def start(task_id, params: VideoParams):
|
|||||||
if sub_maker is None:
|
if sub_maker is None:
|
||||||
sm.state.update_task(task_id, state=const.TASK_STATE_FAILED)
|
sm.state.update_task(task_id, state=const.TASK_STATE_FAILED)
|
||||||
logger.error(
|
logger.error(
|
||||||
"failed to generate audio, maybe the network is not available. if you are in China, please use a VPN.")
|
"""failed to generate audio:
|
||||||
|
1. check if the language of the voice matches the language of the video script.
|
||||||
|
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.
|
||||||
|
""".strip()
|
||||||
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
audio_duration = voice.get_audio_duration(sub_maker)
|
audio_duration = voice.get_audio_duration(sub_maker)
|
||||||
@@ -110,9 +124,24 @@ def start(task_id, params: VideoParams):
|
|||||||
|
|
||||||
sm.state.update_task(task_id, state=const.TASK_STATE_PROCESSING, progress=40)
|
sm.state.update_task(task_id, state=const.TASK_STATE_PROCESSING, progress=40)
|
||||||
|
|
||||||
logger.info("\n\n## downloading videos")
|
downloaded_videos = []
|
||||||
|
if params.video_source == "local":
|
||||||
|
logger.info("\n\n## preprocess local materials")
|
||||||
|
materials = video.preprocess_video(materials=params.video_materials, clip_duration=max_clip_duration)
|
||||||
|
print(materials)
|
||||||
|
|
||||||
|
if not materials:
|
||||||
|
sm.state.update_task(task_id, state=const.TASK_STATE_FAILED)
|
||||||
|
logger.error("no valid materials found, please check the materials and try again.")
|
||||||
|
return
|
||||||
|
for material_info in materials:
|
||||||
|
print(material_info)
|
||||||
|
downloaded_videos.append(material_info.url)
|
||||||
|
else:
|
||||||
|
logger.info(f"\n\n## downloading videos from {params.video_source}")
|
||||||
downloaded_videos = material.download_videos(task_id=task_id,
|
downloaded_videos = material.download_videos(task_id=task_id,
|
||||||
search_terms=video_terms,
|
search_terms=video_terms,
|
||||||
|
source=params.video_source,
|
||||||
video_aspect=params.video_aspect,
|
video_aspect=params.video_aspect,
|
||||||
video_contact_mode=params.video_concat_mode,
|
video_contact_mode=params.video_concat_mode,
|
||||||
audio_duration=audio_duration * params.video_count,
|
audio_duration=audio_duration * params.video_count,
|
||||||
@@ -173,3 +202,8 @@ def start(task_id, params: VideoParams):
|
|||||||
}
|
}
|
||||||
sm.state.update_task(task_id, state=const.TASK_STATE_COMPLETE, progress=100, **kwargs)
|
sm.state.update_task(task_id, state=const.TASK_STATE_COMPLETE, progress=100, **kwargs)
|
||||||
return kwargs
|
return kwargs
|
||||||
|
|
||||||
|
# def start_test(task_id, params: VideoParams):
|
||||||
|
# print(f"start task {task_id} \n")
|
||||||
|
# time.sleep(5)
|
||||||
|
# print(f"task {task_id} finished \n")
|
||||||
|
|||||||
@@ -1,27 +1,29 @@
|
|||||||
import glob
|
import glob
|
||||||
import random
|
import random
|
||||||
from typing import List
|
from typing import List
|
||||||
from PIL import ImageFont
|
from PIL import ImageFont, Image
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
from moviepy.editor import *
|
from moviepy.editor import *
|
||||||
from moviepy.video.tools.subtitles import SubtitlesClip
|
from moviepy.video.tools.subtitles import SubtitlesClip
|
||||||
|
|
||||||
from app.models.schema import VideoAspect, VideoParams, VideoConcatMode
|
from app.models import const
|
||||||
|
from app.models.schema import VideoAspect, VideoParams, VideoConcatMode, MaterialInfo
|
||||||
from app.utils import utils
|
from app.utils import utils
|
||||||
|
|
||||||
|
|
||||||
def get_bgm_file(bgm_type: str = "random", bgm_file: str = ""):
|
def get_bgm_file(bgm_type: str = "random", bgm_file: str = ""):
|
||||||
if not bgm_type:
|
if not bgm_type:
|
||||||
return ""
|
return ""
|
||||||
|
|
||||||
|
if bgm_file and os.path.exists(bgm_file):
|
||||||
|
return bgm_file
|
||||||
|
|
||||||
if bgm_type == "random":
|
if bgm_type == "random":
|
||||||
suffix = "*.mp3"
|
suffix = "*.mp3"
|
||||||
song_dir = utils.song_dir()
|
song_dir = utils.song_dir()
|
||||||
files = glob.glob(os.path.join(song_dir, suffix))
|
files = glob.glob(os.path.join(song_dir, suffix))
|
||||||
return random.choice(files)
|
return random.choice(files)
|
||||||
|
|
||||||
if os.path.exists(bgm_file):
|
|
||||||
return bgm_file
|
|
||||||
|
|
||||||
return ""
|
return ""
|
||||||
|
|
||||||
|
|
||||||
@@ -47,14 +49,29 @@ def combine_videos(combined_video_path: str,
|
|||||||
|
|
||||||
clips = []
|
clips = []
|
||||||
video_duration = 0
|
video_duration = 0
|
||||||
# Add downloaded clips over and over until the duration of the audio (max_duration) has been reached
|
|
||||||
while video_duration < audio_duration:
|
|
||||||
# random video_paths order
|
|
||||||
if video_concat_mode.value == VideoConcatMode.random.value:
|
|
||||||
random.shuffle(video_paths)
|
|
||||||
|
|
||||||
|
raw_clips = []
|
||||||
for video_path in video_paths:
|
for video_path in video_paths:
|
||||||
clip = VideoFileClip(video_path).without_audio()
|
clip = VideoFileClip(video_path).without_audio()
|
||||||
|
clip_duration = clip.duration
|
||||||
|
start_time = 0
|
||||||
|
|
||||||
|
while start_time < clip_duration:
|
||||||
|
end_time = min(start_time + max_clip_duration, clip_duration)
|
||||||
|
split_clip = clip.subclip(start_time, end_time)
|
||||||
|
raw_clips.append(split_clip)
|
||||||
|
# logger.info(f"splitting from {start_time:.2f} to {end_time:.2f}, clip duration {clip_duration:.2f}, split_clip duration {split_clip.duration:.2f}")
|
||||||
|
start_time = end_time
|
||||||
|
if video_concat_mode.value == VideoConcatMode.sequential.value:
|
||||||
|
break
|
||||||
|
|
||||||
|
# random video_paths order
|
||||||
|
if video_concat_mode.value == VideoConcatMode.random.value:
|
||||||
|
random.shuffle(raw_clips)
|
||||||
|
|
||||||
|
# Add downloaded clips over and over until the duration of the audio (max_duration) has been reached
|
||||||
|
while video_duration < audio_duration:
|
||||||
|
for clip in raw_clips:
|
||||||
# Check if clip is longer than the remaining audio
|
# Check if clip is longer than the remaining audio
|
||||||
if (audio_duration - video_duration) < clip.duration:
|
if (audio_duration - video_duration) < clip.duration:
|
||||||
clip = clip.subclip(0, (audio_duration - video_duration))
|
clip = clip.subclip(0, (audio_duration - video_duration))
|
||||||
@@ -99,16 +116,18 @@ def combine_videos(combined_video_path: str,
|
|||||||
clips.append(clip)
|
clips.append(clip)
|
||||||
video_duration += clip.duration
|
video_duration += clip.duration
|
||||||
|
|
||||||
final_clip = concatenate_videoclips(clips)
|
video_clip = concatenate_videoclips(clips)
|
||||||
final_clip = final_clip.set_fps(30)
|
video_clip = video_clip.set_fps(30)
|
||||||
logger.info(f"writing")
|
logger.info(f"writing")
|
||||||
# https://github.com/harry0703/MoneyPrinterTurbo/issues/111#issuecomment-2032354030
|
# https://github.com/harry0703/MoneyPrinterTurbo/issues/111#issuecomment-2032354030
|
||||||
final_clip.write_videofile(filename=combined_video_path,
|
video_clip.write_videofile(filename=combined_video_path,
|
||||||
threads=threads,
|
threads=threads,
|
||||||
logger=None,
|
logger=None,
|
||||||
temp_audiofile_path=output_dir,
|
temp_audiofile_path=output_dir,
|
||||||
audio_codec="aac",
|
audio_codec="aac",
|
||||||
|
fps=30,
|
||||||
)
|
)
|
||||||
|
video_clip.close()
|
||||||
logger.success(f"completed")
|
logger.success(f"completed")
|
||||||
return combined_video_path
|
return combined_video_path
|
||||||
|
|
||||||
@@ -126,7 +145,7 @@ def wrap_text(text, max_width, font='Arial', fontsize=60):
|
|||||||
if width <= max_width:
|
if width <= max_width:
|
||||||
return text, height
|
return text, height
|
||||||
|
|
||||||
logger.warning(f"wrapping text, max_width: {max_width}, text_width: {width}, text: {text}")
|
# logger.warning(f"wrapping text, max_width: {max_width}, text_width: {width}, text: {text}")
|
||||||
|
|
||||||
processed = True
|
processed = True
|
||||||
|
|
||||||
@@ -150,7 +169,7 @@ def wrap_text(text, max_width, font='Arial', fontsize=60):
|
|||||||
_wrapped_lines_ = [line.strip() for line in _wrapped_lines_]
|
_wrapped_lines_ = [line.strip() for line in _wrapped_lines_]
|
||||||
result = '\n'.join(_wrapped_lines_).strip()
|
result = '\n'.join(_wrapped_lines_).strip()
|
||||||
height = len(_wrapped_lines_) * height
|
height = len(_wrapped_lines_) * height
|
||||||
logger.warning(f"wrapped text: {result}")
|
# logger.warning(f"wrapped text: {result}")
|
||||||
return result, height
|
return result, height
|
||||||
|
|
||||||
_wrapped_lines_ = []
|
_wrapped_lines_ = []
|
||||||
@@ -167,7 +186,7 @@ def wrap_text(text, max_width, font='Arial', fontsize=60):
|
|||||||
_wrapped_lines_.append(_txt_)
|
_wrapped_lines_.append(_txt_)
|
||||||
result = '\n'.join(_wrapped_lines_).strip()
|
result = '\n'.join(_wrapped_lines_).strip()
|
||||||
height = len(_wrapped_lines_) * height
|
height = len(_wrapped_lines_) * height
|
||||||
logger.warning(f"wrapped text: {result}")
|
# logger.warning(f"wrapped text: {result}")
|
||||||
return result, height
|
return result, height
|
||||||
|
|
||||||
|
|
||||||
@@ -244,71 +263,122 @@ def generate_video(video_path: str,
|
|||||||
|
|
||||||
bgm_file = get_bgm_file(bgm_type=params.bgm_type, bgm_file=params.bgm_file)
|
bgm_file = get_bgm_file(bgm_type=params.bgm_type, bgm_file=params.bgm_file)
|
||||||
if bgm_file:
|
if bgm_file:
|
||||||
|
try:
|
||||||
bgm_clip = (AudioFileClip(bgm_file)
|
bgm_clip = (AudioFileClip(bgm_file)
|
||||||
.set_duration(video_clip.duration)
|
|
||||||
.volumex(params.bgm_volume)
|
.volumex(params.bgm_volume)
|
||||||
.audio_fadeout(3))
|
.audio_fadeout(3))
|
||||||
|
bgm_clip = afx.audio_loop(bgm_clip, duration=video_clip.duration)
|
||||||
audio_clip = CompositeAudioClip([audio_clip, bgm_clip])
|
audio_clip = CompositeAudioClip([audio_clip, bgm_clip])
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"failed to add bgm: {str(e)}")
|
||||||
|
|
||||||
video_clip = video_clip.set_audio(audio_clip)
|
video_clip = video_clip.set_audio(audio_clip)
|
||||||
video_clip.write_videofile(output_file,
|
video_clip.write_videofile(output_file,
|
||||||
audio_codec="aac",
|
audio_codec="aac",
|
||||||
temp_audiofile_path=output_dir,
|
temp_audiofile_path=output_dir,
|
||||||
threads=params.n_threads or 2,
|
threads=params.n_threads or 2,
|
||||||
logger=None)
|
logger=None,
|
||||||
|
fps=30,
|
||||||
|
)
|
||||||
|
video_clip.close()
|
||||||
logger.success(f"completed")
|
logger.success(f"completed")
|
||||||
|
|
||||||
|
|
||||||
|
def preprocess_video(materials: List[MaterialInfo], clip_duration=4):
|
||||||
|
for material in materials:
|
||||||
|
if not material.url:
|
||||||
|
continue
|
||||||
|
|
||||||
|
ext = utils.parse_extension(material.url)
|
||||||
|
try:
|
||||||
|
clip = VideoFileClip(material.url)
|
||||||
|
except Exception as e:
|
||||||
|
clip = ImageClip(material.url)
|
||||||
|
|
||||||
|
width = clip.size[0]
|
||||||
|
height = clip.size[1]
|
||||||
|
if width < 480 or height < 480:
|
||||||
|
logger.warning(f"video is too small, width: {width}, height: {height}")
|
||||||
|
continue
|
||||||
|
|
||||||
|
if ext in const.FILE_TYPE_IMAGES:
|
||||||
|
logger.info(f"processing image: {material.url}")
|
||||||
|
# 创建一个图片剪辑,并设置持续时间为3秒钟
|
||||||
|
clip = ImageClip(material.url).set_duration(clip_duration).set_position("center")
|
||||||
|
# 使用resize方法来添加缩放效果。这里使用了lambda函数来使得缩放效果随时间变化。
|
||||||
|
# 假设我们想要从原始大小逐渐放大到120%的大小。
|
||||||
|
# t代表当前时间,clip.duration为视频总时长,这里是3秒。
|
||||||
|
# 注意:1 表示100%的大小,所以1.2表示120%的大小
|
||||||
|
zoom_clip = clip.resize(lambda t: 1 + (clip_duration * 0.03) * (t / clip.duration))
|
||||||
|
|
||||||
|
# 如果需要,可以创建一个包含缩放剪辑的复合视频剪辑
|
||||||
|
# (这在您想要在视频中添加其他元素时非常有用)
|
||||||
|
final_clip = CompositeVideoClip([zoom_clip])
|
||||||
|
|
||||||
|
# 输出视频
|
||||||
|
video_file = f"{material.url}.mp4"
|
||||||
|
final_clip.write_videofile(video_file, fps=30, logger=None)
|
||||||
|
final_clip.close()
|
||||||
|
material.url = video_file
|
||||||
|
logger.success(f"completed: {video_file}")
|
||||||
|
return materials
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
txt_en = "Here's your guide to travel hacks for budget-friendly adventures"
|
m = MaterialInfo()
|
||||||
txt_zh = "测试长字段这是您的旅行技巧指南帮助您进行预算友好的冒险"
|
m.url = "/Users/harry/Downloads/IMG_2915.JPG"
|
||||||
font = utils.resource_dir() + "/fonts/STHeitiMedium.ttc"
|
m.provider = "local"
|
||||||
for txt in [txt_en, txt_zh]:
|
materials = preprocess_video([m], clip_duration=4)
|
||||||
t, h = wrap_text(text=txt, max_width=1000, font=font, fontsize=60)
|
print(materials)
|
||||||
print(t)
|
|
||||||
|
|
||||||
task_id = "aa563149-a7ea-49c2-b39f-8c32cc225baf"
|
# txt_en = "Here's your guide to travel hacks for budget-friendly adventures"
|
||||||
task_dir = utils.task_dir(task_id)
|
# txt_zh = "测试长字段这是您的旅行技巧指南帮助您进行预算友好的冒险"
|
||||||
video_file = f"{task_dir}/combined-1.mp4"
|
# font = utils.resource_dir() + "/fonts/STHeitiMedium.ttc"
|
||||||
audio_file = f"{task_dir}/audio.mp3"
|
# for txt in [txt_en, txt_zh]:
|
||||||
subtitle_file = f"{task_dir}/subtitle.srt"
|
# t, h = wrap_text(text=txt, max_width=1000, font=font, fontsize=60)
|
||||||
output_file = f"{task_dir}/final.mp4"
|
# print(t)
|
||||||
|
|
||||||
# video_paths = []
|
|
||||||
# for file in os.listdir(utils.storage_dir("test")):
|
|
||||||
# if file.endswith(".mp4"):
|
|
||||||
# video_paths.append(os.path.join(utils.storage_dir("test"), file))
|
|
||||||
#
|
#
|
||||||
# combine_videos(combined_video_path=video_file,
|
# task_id = "aa563149-a7ea-49c2-b39f-8c32cc225baf"
|
||||||
# audio_file=audio_file,
|
# task_dir = utils.task_dir(task_id)
|
||||||
# video_paths=video_paths,
|
# video_file = f"{task_dir}/combined-1.mp4"
|
||||||
# video_aspect=VideoAspect.portrait,
|
# audio_file = f"{task_dir}/audio.mp3"
|
||||||
# video_concat_mode=VideoConcatMode.random,
|
# subtitle_file = f"{task_dir}/subtitle.srt"
|
||||||
# max_clip_duration=5,
|
# output_file = f"{task_dir}/final.mp4"
|
||||||
# threads=2)
|
#
|
||||||
|
# # video_paths = []
|
||||||
cfg = VideoParams()
|
# # for file in os.listdir(utils.storage_dir("test")):
|
||||||
cfg.video_aspect = VideoAspect.portrait
|
# # if file.endswith(".mp4"):
|
||||||
cfg.font_name = "STHeitiMedium.ttc"
|
# # video_paths.append(os.path.join(utils.storage_dir("test"), file))
|
||||||
cfg.font_size = 60
|
# #
|
||||||
cfg.stroke_color = "#000000"
|
# # combine_videos(combined_video_path=video_file,
|
||||||
cfg.stroke_width = 1.5
|
# # audio_file=audio_file,
|
||||||
cfg.text_fore_color = "#FFFFFF"
|
# # video_paths=video_paths,
|
||||||
cfg.text_background_color = "transparent"
|
# # video_aspect=VideoAspect.portrait,
|
||||||
cfg.bgm_type = "random"
|
# # video_concat_mode=VideoConcatMode.random,
|
||||||
cfg.bgm_file = ""
|
# # max_clip_duration=5,
|
||||||
cfg.bgm_volume = 1.0
|
# # threads=2)
|
||||||
cfg.subtitle_enabled = True
|
#
|
||||||
cfg.subtitle_position = "bottom"
|
# cfg = VideoParams()
|
||||||
cfg.n_threads = 2
|
# cfg.video_aspect = VideoAspect.portrait
|
||||||
cfg.paragraph_number = 1
|
# cfg.font_name = "STHeitiMedium.ttc"
|
||||||
|
# cfg.font_size = 60
|
||||||
cfg.voice_volume = 1.0
|
# cfg.stroke_color = "#000000"
|
||||||
|
# cfg.stroke_width = 1.5
|
||||||
generate_video(video_path=video_file,
|
# cfg.text_fore_color = "#FFFFFF"
|
||||||
audio_path=audio_file,
|
# cfg.text_background_color = "transparent"
|
||||||
subtitle_path=subtitle_file,
|
# cfg.bgm_type = "random"
|
||||||
output_file=output_file,
|
# cfg.bgm_file = ""
|
||||||
params=cfg
|
# cfg.bgm_volume = 1.0
|
||||||
)
|
# cfg.subtitle_enabled = True
|
||||||
|
# cfg.subtitle_position = "bottom"
|
||||||
|
# cfg.n_threads = 2
|
||||||
|
# cfg.paragraph_number = 1
|
||||||
|
#
|
||||||
|
# cfg.voice_volume = 1.0
|
||||||
|
#
|
||||||
|
# generate_video(video_path=video_file,
|
||||||
|
# audio_path=audio_file,
|
||||||
|
# subtitle_path=subtitle_file,
|
||||||
|
# output_file=output_file,
|
||||||
|
# params=cfg
|
||||||
|
# )
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import asyncio
|
import asyncio
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
|
from datetime import datetime
|
||||||
from xml.sax.saxutils import unescape
|
from xml.sax.saxutils import unescape
|
||||||
from edge_tts.submaker import mktimestamp
|
from edge_tts.submaker import mktimestamp
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
@@ -8,12 +9,13 @@ from edge_tts import submaker, SubMaker
|
|||||||
import edge_tts
|
import edge_tts
|
||||||
from moviepy.video.tools import subtitles
|
from moviepy.video.tools import subtitles
|
||||||
|
|
||||||
|
from app.config import config
|
||||||
from app.utils import utils
|
from app.utils import utils
|
||||||
|
|
||||||
|
|
||||||
def get_all_voices(filter_locals=None) -> list[str]:
|
def get_all_azure_voices(filter_locals=None) -> list[str]:
|
||||||
if filter_locals is None:
|
if filter_locals is None:
|
||||||
filter_locals = ["zh-CN", "en-US", "zh-HK", "zh-TW"]
|
filter_locals = ["zh-CN", "en-US", "zh-HK", "zh-TW", "vi-VN"]
|
||||||
voices_str = """
|
voices_str = """
|
||||||
Name: af-ZA-AdriNeural
|
Name: af-ZA-AdriNeural
|
||||||
Gender: Female
|
Gender: Female
|
||||||
@@ -956,6 +958,34 @@ Gender: Female
|
|||||||
|
|
||||||
Name: zu-ZA-ThembaNeural
|
Name: zu-ZA-ThembaNeural
|
||||||
Gender: Male
|
Gender: Male
|
||||||
|
|
||||||
|
|
||||||
|
Name: en-US-AvaMultilingualNeural-V2
|
||||||
|
Gender: Female
|
||||||
|
|
||||||
|
Name: en-US-AndrewMultilingualNeural-V2
|
||||||
|
Gender: Male
|
||||||
|
|
||||||
|
Name: en-US-EmmaMultilingualNeural-V2
|
||||||
|
Gender: Female
|
||||||
|
|
||||||
|
Name: en-US-BrianMultilingualNeural-V2
|
||||||
|
Gender: Male
|
||||||
|
|
||||||
|
Name: de-DE-FlorianMultilingualNeural-V2
|
||||||
|
Gender: Male
|
||||||
|
|
||||||
|
Name: de-DE-SeraphinaMultilingualNeural-V2
|
||||||
|
Gender: Female
|
||||||
|
|
||||||
|
Name: fr-FR-RemyMultilingualNeural-V2
|
||||||
|
Gender: Male
|
||||||
|
|
||||||
|
Name: fr-FR-VivienneMultilingualNeural-V2
|
||||||
|
Gender: Female
|
||||||
|
|
||||||
|
Name: zh-CN-XiaoxiaoMultilingualNeural-V2
|
||||||
|
Gender: Female
|
||||||
""".strip()
|
""".strip()
|
||||||
voices = []
|
voices = []
|
||||||
name = ''
|
name = ''
|
||||||
@@ -986,11 +1016,26 @@ Gender: Male
|
|||||||
def parse_voice_name(name: str):
|
def parse_voice_name(name: str):
|
||||||
# zh-CN-XiaoyiNeural-Female
|
# zh-CN-XiaoyiNeural-Female
|
||||||
# zh-CN-YunxiNeural-Male
|
# zh-CN-YunxiNeural-Male
|
||||||
|
# zh-CN-XiaoxiaoMultilingualNeural-V2-Female
|
||||||
name = name.replace("-Female", "").replace("-Male", "").strip()
|
name = name.replace("-Female", "").replace("-Male", "").strip()
|
||||||
return name
|
return name
|
||||||
|
|
||||||
|
|
||||||
|
def is_azure_v2_voice(voice_name: str):
|
||||||
|
voice_name = parse_voice_name(voice_name)
|
||||||
|
if voice_name.endswith("-V2"):
|
||||||
|
return voice_name.replace("-V2", "").strip()
|
||||||
|
return ""
|
||||||
|
|
||||||
|
|
||||||
def tts(text: str, voice_name: str, voice_file: str) -> [SubMaker, None]:
|
def tts(text: str, voice_name: str, voice_file: str) -> [SubMaker, None]:
|
||||||
|
if is_azure_v2_voice(voice_name):
|
||||||
|
return azure_tts_v2(text, voice_name, voice_file)
|
||||||
|
return azure_tts_v1(text, voice_name, voice_file)
|
||||||
|
|
||||||
|
|
||||||
|
def azure_tts_v1(text: str, voice_name: str, voice_file: str) -> [SubMaker, None]:
|
||||||
|
voice_name = parse_voice_name(voice_name)
|
||||||
text = text.strip()
|
text = text.strip()
|
||||||
for i in range(3):
|
for i in range(3):
|
||||||
try:
|
try:
|
||||||
@@ -1019,14 +1064,82 @@ def tts(text: str, voice_name: str, voice_file: str) -> [SubMaker, None]:
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
def create_subtitle(sub_maker: submaker.SubMaker, text: str, subtitle_file: str):
|
def azure_tts_v2(text: str, voice_name: str, voice_file: str) -> [SubMaker, None]:
|
||||||
"""
|
voice_name = is_azure_v2_voice(voice_name)
|
||||||
优化字幕文件
|
if not voice_name:
|
||||||
1. 将字幕文件按照标点符号分割成多行
|
logger.error(f"invalid voice name: {voice_name}")
|
||||||
2. 逐行匹配字幕文件中的文本
|
raise ValueError(f"invalid voice name: {voice_name}")
|
||||||
3. 生成新的字幕文件
|
text = text.strip()
|
||||||
"""
|
|
||||||
text = text.replace("\n", " ")
|
def _format_duration_to_offset(duration) -> int:
|
||||||
|
if isinstance(duration, str):
|
||||||
|
time_obj = datetime.strptime(duration, "%H:%M:%S.%f")
|
||||||
|
milliseconds = (time_obj.hour * 3600000) + (time_obj.minute * 60000) + (time_obj.second * 1000) + (
|
||||||
|
time_obj.microsecond // 1000)
|
||||||
|
return milliseconds * 10000
|
||||||
|
|
||||||
|
if isinstance(duration, int):
|
||||||
|
return duration
|
||||||
|
|
||||||
|
return 0
|
||||||
|
|
||||||
|
for i in range(3):
|
||||||
|
try:
|
||||||
|
logger.info(f"start, voice name: {voice_name}, try: {i + 1}")
|
||||||
|
|
||||||
|
import azure.cognitiveservices.speech as speechsdk
|
||||||
|
|
||||||
|
sub_maker = SubMaker()
|
||||||
|
|
||||||
|
def speech_synthesizer_word_boundary_cb(evt: speechsdk.SessionEventArgs):
|
||||||
|
# print('WordBoundary event:')
|
||||||
|
# print('\tBoundaryType: {}'.format(evt.boundary_type))
|
||||||
|
# print('\tAudioOffset: {}ms'.format((evt.audio_offset + 5000)))
|
||||||
|
# print('\tDuration: {}'.format(evt.duration))
|
||||||
|
# print('\tText: {}'.format(evt.text))
|
||||||
|
# print('\tTextOffset: {}'.format(evt.text_offset))
|
||||||
|
# print('\tWordLength: {}'.format(evt.word_length))
|
||||||
|
|
||||||
|
duration = _format_duration_to_offset(str(evt.duration))
|
||||||
|
offset = _format_duration_to_offset(evt.audio_offset)
|
||||||
|
sub_maker.subs.append(evt.text)
|
||||||
|
sub_maker.offset.append((offset, offset + duration))
|
||||||
|
|
||||||
|
# Creates an instance of a speech config with specified subscription key and service region.
|
||||||
|
speech_key = config.azure.get("speech_key", "")
|
||||||
|
service_region = config.azure.get("speech_region", "")
|
||||||
|
audio_config = speechsdk.audio.AudioOutputConfig(filename=voice_file, use_default_speaker=True)
|
||||||
|
speech_config = speechsdk.SpeechConfig(subscription=speech_key,
|
||||||
|
region=service_region)
|
||||||
|
speech_config.speech_synthesis_voice_name = voice_name
|
||||||
|
# speech_config.set_property(property_id=speechsdk.PropertyId.SpeechServiceResponse_RequestSentenceBoundary,
|
||||||
|
# value='true')
|
||||||
|
speech_config.set_property(property_id=speechsdk.PropertyId.SpeechServiceResponse_RequestWordBoundary,
|
||||||
|
value='true')
|
||||||
|
|
||||||
|
speech_config.set_speech_synthesis_output_format(
|
||||||
|
speechsdk.SpeechSynthesisOutputFormat.Audio48Khz192KBitRateMonoMp3)
|
||||||
|
speech_synthesizer = speechsdk.SpeechSynthesizer(audio_config=audio_config,
|
||||||
|
speech_config=speech_config)
|
||||||
|
speech_synthesizer.synthesis_word_boundary.connect(speech_synthesizer_word_boundary_cb)
|
||||||
|
|
||||||
|
result = speech_synthesizer.speak_text_async(text).get()
|
||||||
|
if result.reason == speechsdk.ResultReason.SynthesizingAudioCompleted:
|
||||||
|
logger.success(f"azure v2 speech synthesis succeeded: {voice_file}")
|
||||||
|
return sub_maker
|
||||||
|
elif result.reason == speechsdk.ResultReason.Canceled:
|
||||||
|
cancellation_details = result.cancellation_details
|
||||||
|
logger.error(f"azure v2 speech synthesis canceled: {cancellation_details.reason}")
|
||||||
|
if cancellation_details.reason == speechsdk.CancellationReason.Error:
|
||||||
|
logger.error(f"azure v2 speech synthesis error: {cancellation_details.error_details}")
|
||||||
|
logger.info(f"completed, output file: {voice_file}")
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"failed, error: {str(e)}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _format_text(text: str) -> str:
|
||||||
|
# text = text.replace("\n", " ")
|
||||||
text = text.replace("[", " ")
|
text = text.replace("[", " ")
|
||||||
text = text.replace("]", " ")
|
text = text.replace("]", " ")
|
||||||
text = text.replace("(", " ")
|
text = text.replace("(", " ")
|
||||||
@@ -1034,6 +1147,18 @@ def create_subtitle(sub_maker: submaker.SubMaker, text: str, subtitle_file: str)
|
|||||||
text = text.replace("{", " ")
|
text = text.replace("{", " ")
|
||||||
text = text.replace("}", " ")
|
text = text.replace("}", " ")
|
||||||
text = text.strip()
|
text = text.strip()
|
||||||
|
return text
|
||||||
|
|
||||||
|
|
||||||
|
def create_subtitle(sub_maker: submaker.SubMaker, text: str, subtitle_file: str):
|
||||||
|
"""
|
||||||
|
优化字幕文件
|
||||||
|
1. 将字幕文件按照标点符号分割成多行
|
||||||
|
2. 逐行匹配字幕文件中的文本
|
||||||
|
3. 生成新的字幕文件
|
||||||
|
"""
|
||||||
|
|
||||||
|
text = _format_text(text)
|
||||||
|
|
||||||
def formatter(idx: int, start_time: float, end_time: float, sub_text: str) -> str:
|
def formatter(idx: int, start_time: float, end_time: float, sub_text: str) -> str:
|
||||||
"""
|
"""
|
||||||
@@ -1125,8 +1250,12 @@ def get_audio_duration(sub_maker: submaker.SubMaker):
|
|||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
voices = get_all_voices()
|
voice_name = "zh-CN-XiaoxiaoMultilingualNeural-V2-Female"
|
||||||
print(voices)
|
voice_name = parse_voice_name(voice_name)
|
||||||
|
voice_name = is_azure_v2_voice(voice_name)
|
||||||
|
print(voice_name)
|
||||||
|
|
||||||
|
voices = get_all_azure_voices()
|
||||||
print(len(voices))
|
print(len(voices))
|
||||||
|
|
||||||
|
|
||||||
@@ -1134,6 +1263,7 @@ if __name__ == "__main__":
|
|||||||
temp_dir = utils.storage_dir("temp")
|
temp_dir = utils.storage_dir("temp")
|
||||||
|
|
||||||
voice_names = [
|
voice_names = [
|
||||||
|
"zh-CN-XiaoxiaoMultilingualNeural",
|
||||||
# 女性
|
# 女性
|
||||||
"zh-CN-XiaoxiaoNeural",
|
"zh-CN-XiaoxiaoNeural",
|
||||||
"zh-CN-XiaoyiNeural",
|
"zh-CN-XiaoyiNeural",
|
||||||
@@ -1156,10 +1286,28 @@ if __name__ == "__main__":
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
text = "[Opening scene: A sunny day in a suburban neighborhood. A young boy named Alex, around 8 years old, is playing in his front yard with his loyal dog, Buddy.]\n\n[Camera zooms in on Alex as he throws a ball for Buddy to fetch. Buddy excitedly runs after it and brings it back to Alex.]\n\nAlex: Good boy, Buddy! You're the best dog ever!\n\n[Buddy barks happily and wags his tail.]\n\n[As Alex and Buddy continue playing, a series of potential dangers loom nearby, such as a stray dog approaching, a ball rolling towards the street, and a suspicious-looking stranger walking by.]\n\nAlex: Uh oh, Buddy, look out!\n\n[Buddy senses the danger and immediately springs into action. He barks loudly at the stray dog, scaring it away. Then, he rushes to retrieve the ball before it reaches the street and gently nudges it back towards Alex. Finally, he stands protectively between Alex and the stranger, growling softly to warn them away.]\n\nAlex: Wow, Buddy, you're like my superhero!\n\n[Just as Alex and Buddy are about to head inside, they hear a loud crash from a nearby construction site. They rush over to investigate and find a pile of rubble blocking the path of a kitten trapped underneath.]\n\nAlex: Oh no, Buddy, we have to help!\n\n[Buddy barks in agreement and together they work to carefully move the rubble aside, allowing the kitten to escape unharmed. The kitten gratefully nuzzles against Buddy, who responds with a friendly lick.]\n\nAlex: We did it, Buddy! We saved the day again!\n\n[As Alex and Buddy walk home together, the sun begins to set, casting a warm glow over the neighborhood.]\n\nAlex: Thanks for always being there to watch over me, Buddy. You're not just my dog, you're my best friend.\n\n[Buddy barks happily and nuzzles against Alex as they disappear into the sunset, ready to face whatever adventures tomorrow may bring.]\n\n[End scene.]"
|
text = "[Opening scene: A sunny day in a suburban neighborhood. A young boy named Alex, around 8 years old, is playing in his front yard with his loyal dog, Buddy.]\n\n[Camera zooms in on Alex as he throws a ball for Buddy to fetch. Buddy excitedly runs after it and brings it back to Alex.]\n\nAlex: Good boy, Buddy! You're the best dog ever!\n\n[Buddy barks happily and wags his tail.]\n\n[As Alex and Buddy continue playing, a series of potential dangers loom nearby, such as a stray dog approaching, a ball rolling towards the street, and a suspicious-looking stranger walking by.]\n\nAlex: Uh oh, Buddy, look out!\n\n[Buddy senses the danger and immediately springs into action. He barks loudly at the stray dog, scaring it away. Then, he rushes to retrieve the ball before it reaches the street and gently nudges it back towards Alex. Finally, he stands protectively between Alex and the stranger, growling softly to warn them away.]\n\nAlex: Wow, Buddy, you're like my superhero!\n\n[Just as Alex and Buddy are about to head inside, they hear a loud crash from a nearby construction site. They rush over to investigate and find a pile of rubble blocking the path of a kitten trapped underneath.]\n\nAlex: Oh no, Buddy, we have to help!\n\n[Buddy barks in agreement and together they work to carefully move the rubble aside, allowing the kitten to escape unharmed. The kitten gratefully nuzzles against Buddy, who responds with a friendly lick.]\n\nAlex: We did it, Buddy! We saved the day again!\n\n[As Alex and Buddy walk home together, the sun begins to set, casting a warm glow over the neighborhood.]\n\nAlex: Thanks for always being there to watch over me, Buddy. You're not just my dog, you're my best friend.\n\n[Buddy barks happily and nuzzles against Alex as they disappear into the sunset, ready to face whatever adventures tomorrow may bring.]\n\n[End scene.]"
|
||||||
|
|
||||||
|
text = "大家好,我是乔哥,一个想帮你把信用卡全部还清的家伙!\n今天我们要聊的是信用卡的取现功能。\n你是不是也曾经因为一时的资金紧张,而拿着信用卡到ATM机取现?如果是,那你得好好看看这个视频了。\n现在都2024年了,我以为现在不会再有人用信用卡取现功能了。前几天一个粉丝发来一张图片,取现1万。\n信用卡取现有三个弊端。\n一,信用卡取现功能代价可不小。会先收取一个取现手续费,比如这个粉丝,取现1万,按2.5%收取手续费,收取了250元。\n二,信用卡正常消费有最长56天的免息期,但取现不享受免息期。从取现那一天开始,每天按照万5收取利息,这个粉丝用了11天,收取了55元利息。\n三,频繁的取现行为,银行会认为你资金紧张,会被标记为高风险用户,影响你的综合评分和额度。\n那么,如果你资金紧张了,该怎么办呢?\n乔哥给你支一招,用破思机摩擦信用卡,只需要少量的手续费,而且还可以享受最长56天的免息期。\n最后,如果你对玩卡感兴趣,可以找乔哥领取一本《卡神秘籍》,用卡过程中遇到任何疑惑,也欢迎找乔哥交流。\n别忘了,关注乔哥,回复用卡技巧,免费领取《2024用卡技巧》,让我们一起成为用卡高手!"
|
||||||
|
|
||||||
|
text = """
|
||||||
|
2023全年业绩速览
|
||||||
|
公司全年累计实现营业收入1476.94亿元,同比增长19.01%,归母净利润747.34亿元,同比增长19.16%。EPS达到59.49元。第四季度单季,营业收入444.25亿元,同比增长20.26%,环比增长31.86%;归母净利润218.58亿元,同比增长19.33%,环比增长29.37%。这一阶段
|
||||||
|
的业绩表现不仅突显了公司的增长动力和盈利能力,也反映出公司在竞争激烈的市场环境中保持了良好的发展势头。
|
||||||
|
2023年Q4业绩速览
|
||||||
|
第四季度,营业收入贡献主要增长点;销售费用高增致盈利能力承压;税金同比上升27%,扰动净利率表现。
|
||||||
|
业绩解读
|
||||||
|
利润方面,2023全年贵州茅台,>归母净利润增速为19%,其中营业收入正贡献18%,营业成本正贡献百分之一,管理费用正贡献百分之一点四。(注:归母净利润增速值=营业收入增速+各科目贡献,展示贡献/拖累的前四名科目,且要求贡献值/净利润增速>15%)
|
||||||
|
"""
|
||||||
|
text = "静夜思是唐代诗人李白创作的一首五言古诗。这首诗描绘了诗人在寂静的夜晚,看到窗前的明月,不禁想起远方的家乡和亲人"
|
||||||
|
|
||||||
|
text = _format_text(text)
|
||||||
|
lines = utils.split_string_by_punctuations(text)
|
||||||
|
print(lines)
|
||||||
|
|
||||||
for voice_name in voice_names:
|
for voice_name in voice_names:
|
||||||
voice_file = f"{temp_dir}/tts-{voice_name}.mp3"
|
voice_file = f"{temp_dir}/tts-{voice_name}.mp3"
|
||||||
subtitle_file = f"{temp_dir}/tts.mp3.srt"
|
subtitle_file = f"{temp_dir}/tts.mp3.srt"
|
||||||
sub_maker = tts(text=text, voice_name=voice_name, voice_file=voice_file)
|
sub_maker = azure_tts_v2(text=text, voice_name=voice_name, voice_file=voice_file)
|
||||||
create_subtitle(sub_maker=sub_maker, text=text, subtitle_file=subtitle_file)
|
create_subtitle(sub_maker=sub_maker, text=text, subtitle_file=subtitle_file)
|
||||||
audio_duration = get_audio_duration(sub_maker)
|
audio_duration = get_audio_duration(sub_maker)
|
||||||
print(f"voice: {voice_name}, audio duration: {audio_duration}s")
|
print(f"voice: {voice_name}, audio duration: {audio_duration}s")
|
||||||
|
|||||||
@@ -67,10 +67,13 @@ def root_dir():
|
|||||||
return os.path.dirname(os.path.dirname(os.path.dirname(os.path.realpath(__file__))))
|
return os.path.dirname(os.path.dirname(os.path.dirname(os.path.realpath(__file__))))
|
||||||
|
|
||||||
|
|
||||||
def storage_dir(sub_dir: str = ""):
|
def storage_dir(sub_dir: str = "", create: bool = False):
|
||||||
d = os.path.join(root_dir(), "storage")
|
d = os.path.join(root_dir(), "storage")
|
||||||
if sub_dir:
|
if sub_dir:
|
||||||
d = os.path.join(d, sub_dir)
|
d = os.path.join(d, sub_dir)
|
||||||
|
if create and not os.path.exists(d):
|
||||||
|
os.makedirs(d)
|
||||||
|
|
||||||
return d
|
return d
|
||||||
|
|
||||||
|
|
||||||
@@ -163,12 +166,34 @@ def str_contains_punctuation(word):
|
|||||||
def split_string_by_punctuations(s):
|
def split_string_by_punctuations(s):
|
||||||
result = []
|
result = []
|
||||||
txt = ""
|
txt = ""
|
||||||
for char in s:
|
|
||||||
|
previous_char = ""
|
||||||
|
next_char = ""
|
||||||
|
for i in range(len(s)):
|
||||||
|
char = s[i]
|
||||||
|
if char == "\n":
|
||||||
|
result.append(txt.strip())
|
||||||
|
txt = ""
|
||||||
|
continue
|
||||||
|
|
||||||
|
if i > 0:
|
||||||
|
previous_char = s[i - 1]
|
||||||
|
if i < len(s) - 1:
|
||||||
|
next_char = s[i + 1]
|
||||||
|
|
||||||
|
if char == "." and previous_char.isdigit() and next_char.isdigit():
|
||||||
|
# 取现1万,按2.5%收取手续费, 2.5 中的 . 不能作为换行标记
|
||||||
|
txt += char
|
||||||
|
continue
|
||||||
|
|
||||||
if char not in const.PUNCTUATIONS:
|
if char not in const.PUNCTUATIONS:
|
||||||
txt += char
|
txt += char
|
||||||
else:
|
else:
|
||||||
result.append(txt.strip())
|
result.append(txt.strip())
|
||||||
txt = ""
|
txt = ""
|
||||||
|
result.append(txt.strip())
|
||||||
|
# filter empty string
|
||||||
|
result = list(filter(None, result))
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
@@ -197,3 +222,7 @@ def load_locales(i18n_dir):
|
|||||||
with open(os.path.join(root, file), "r", encoding="utf-8") as f:
|
with open(os.path.join(root, file), "r", encoding="utf-8") as f:
|
||||||
_locales[lang] = json.loads(f.read())
|
_locales[lang] = json.loads(f.read())
|
||||||
return _locales
|
return _locales
|
||||||
|
|
||||||
|
|
||||||
|
def parse_extension(filename):
|
||||||
|
return os.path.splitext(filename)[1].strip().lower().replace(".", "")
|
||||||
|
|||||||
17
changelog.py
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
from git_changelog.cli import build_and_render
|
||||||
|
|
||||||
|
# 运行这段脚本自动生成CHANGELOG.md文件
|
||||||
|
|
||||||
|
build_and_render(
|
||||||
|
repository=".",
|
||||||
|
output="CHANGELOG.md",
|
||||||
|
convention="angular",
|
||||||
|
provider="github",
|
||||||
|
template="keepachangelog",
|
||||||
|
parse_trailers=True,
|
||||||
|
parse_refs=False,
|
||||||
|
sections=["build", "deps", "feat", "fix", "refactor"],
|
||||||
|
versioning="pep440",
|
||||||
|
bump="1.1.2", # 指定bump版本
|
||||||
|
in_place=True,
|
||||||
|
)
|
||||||
@@ -1,4 +1,6 @@
|
|||||||
[app]
|
[app]
|
||||||
|
|
||||||
|
video_source = "pexels" # "pexels" or "pixabay"
|
||||||
# Pexels API Key
|
# Pexels API Key
|
||||||
# Register at https://www.pexels.com/api/ to get your API key.
|
# Register at https://www.pexels.com/api/ to get your API key.
|
||||||
# You can use multiple keys to avoid rate limits.
|
# You can use multiple keys to avoid rate limits.
|
||||||
@@ -6,6 +8,13 @@
|
|||||||
# 特别注意格式,Key 用英文双引号括起来,多个Key用逗号隔开
|
# 特别注意格式,Key 用英文双引号括起来,多个Key用逗号隔开
|
||||||
pexels_api_keys = []
|
pexels_api_keys = []
|
||||||
|
|
||||||
|
# Pixabay API Key
|
||||||
|
# Register at https://pixabay.com/api/docs/ to get your API key.
|
||||||
|
# You can use multiple keys to avoid rate limits.
|
||||||
|
# For example: pixabay_api_keys = ["123adsf4567adf89","abd1321cd13efgfdfhi"]
|
||||||
|
# 特别注意格式,Key 用英文双引号括起来,多个Key用逗号隔开
|
||||||
|
pixabay_api_keys = []
|
||||||
|
|
||||||
# 如果你没有 OPENAI API Key,可以使用 g4f 代替,或者使用国内的 Moonshot API
|
# 如果你没有 OPENAI API Key,可以使用 g4f 代替,或者使用国内的 Moonshot API
|
||||||
# If you don't have an OPENAI API Key, you can use g4f instead
|
# If you don't have an OPENAI API Key, you can use g4f instead
|
||||||
|
|
||||||
@@ -31,7 +40,7 @@
|
|||||||
# No need to set it unless you want to use your own proxy
|
# No need to set it unless you want to use your own proxy
|
||||||
openai_base_url = ""
|
openai_base_url = ""
|
||||||
# Check your available models at https://platform.openai.com/account/limits
|
# Check your available models at https://platform.openai.com/account/limits
|
||||||
openai_model_name = "gpt-4-turbo-preview"
|
openai_model_name = "gpt-4-turbo"
|
||||||
|
|
||||||
########## Moonshot API Key
|
########## Moonshot API Key
|
||||||
# Visit https://platform.moonshot.cn/console/api-keys to get your API key.
|
# Visit https://platform.moonshot.cn/console/api-keys to get your API key.
|
||||||
@@ -48,7 +57,7 @@
|
|||||||
########## G4F
|
########## G4F
|
||||||
# Visit https://github.com/xtekky/gpt4free to get more details
|
# Visit https://github.com/xtekky/gpt4free to get more details
|
||||||
# Supported model list: https://github.com/xtekky/gpt4free/blob/main/g4f/models.py
|
# Supported model list: https://github.com/xtekky/gpt4free/blob/main/g4f/models.py
|
||||||
g4f_model_name = "gpt-3.5-turbo-16k-0613"
|
g4f_model_name = "gpt-3.5-turbo"
|
||||||
|
|
||||||
########## Azure API Key
|
########## Azure API Key
|
||||||
# Visit https://learn.microsoft.com/zh-cn/azure/ai-services/openai/ to get more details
|
# Visit https://learn.microsoft.com/zh-cn/azure/ai-services/openai/ to get more details
|
||||||
@@ -71,6 +80,12 @@
|
|||||||
qwen_model_name = "qwen-max"
|
qwen_model_name = "qwen-max"
|
||||||
|
|
||||||
|
|
||||||
|
########## DeepSeek API Key
|
||||||
|
# Visit https://platform.deepseek.com/api_keys to get your API key
|
||||||
|
deepseek_api_key = ""
|
||||||
|
deepseek_base_url = "https://api.deepseek.com"
|
||||||
|
deepseek_model_name = "deepseek-chat"
|
||||||
|
|
||||||
# Subtitle Provider, "edge" or "whisper"
|
# Subtitle Provider, "edge" or "whisper"
|
||||||
# If empty, the subtitle will not be generated
|
# If empty, the subtitle will not be generated
|
||||||
subtitle_provider = "edge"
|
subtitle_provider = "edge"
|
||||||
@@ -134,6 +149,15 @@
|
|||||||
redis_host = "localhost"
|
redis_host = "localhost"
|
||||||
redis_port = 6379
|
redis_port = 6379
|
||||||
redis_db = 0
|
redis_db = 0
|
||||||
|
redis_password = ""
|
||||||
|
|
||||||
|
# 文生视频时的最大并发任务数
|
||||||
|
max_concurrent_tasks = 5
|
||||||
|
|
||||||
|
# webui界面是否显示配置项
|
||||||
|
# webui hide baisc config panel
|
||||||
|
hide_config = false
|
||||||
|
|
||||||
|
|
||||||
[whisper]
|
[whisper]
|
||||||
# Only effective when subtitle_provider is "whisper"
|
# Only effective when subtitle_provider is "whisper"
|
||||||
@@ -153,12 +177,18 @@
|
|||||||
device="CPU"
|
device="CPU"
|
||||||
compute_type="int8"
|
compute_type="int8"
|
||||||
|
|
||||||
[pexels]
|
|
||||||
video_concat_mode="sequential" # "random" or "sequential"
|
[proxy]
|
||||||
[pexels.proxies]
|
|
||||||
### Use a proxy to access the Pexels API
|
### Use a proxy to access the Pexels API
|
||||||
### Format: "http://<username>:<password>@<proxy>:<port>"
|
### Format: "http://<username>:<password>@<proxy>:<port>"
|
||||||
### Example: "http://user:pass@proxy:1234"
|
### Example: "http://user:pass@proxy:1234"
|
||||||
### Doc: https://requests.readthedocs.io/en/latest/user/advanced/#proxies
|
### Doc: https://requests.readthedocs.io/en/latest/user/advanced/#proxies
|
||||||
|
|
||||||
# http = "http://10.10.1.10:3128"
|
# http = "http://10.10.1.10:3128"
|
||||||
# https = "http://10.10.1.10:1080"
|
# https = "http://10.10.1.10:1080"
|
||||||
|
|
||||||
|
[azure]
|
||||||
|
# Azure Speech API Key
|
||||||
|
# Get your API key at https://portal.azure.com/#view/Microsoft_Azure_ProjectOxford/CognitiveServicesHub/~/SpeechServices
|
||||||
|
speech_key=""
|
||||||
|
speech_region=""
|
||||||
@@ -1,5 +1,3 @@
|
|||||||
version: "3"
|
|
||||||
|
|
||||||
x-common-volumes: &common-volumes
|
x-common-volumes: &common-volumes
|
||||||
- ./:/MoneyPrinterTurbo
|
- ./:/MoneyPrinterTurbo
|
||||||
|
|
||||||
|
|||||||
BIN
docs/picwish.jpg
Normal file
|
After Width: | Height: | Size: 178 KiB |
BIN
docs/wechat-group.jpg
Normal file
|
After Width: | Height: | Size: 190 KiB |
@@ -6,14 +6,21 @@ edge_tts~=6.1.10
|
|||||||
uvicorn~=0.27.1
|
uvicorn~=0.27.1
|
||||||
fastapi~=0.110.0
|
fastapi~=0.110.0
|
||||||
tomli~=2.0.1
|
tomli~=2.0.1
|
||||||
streamlit~=1.32.0
|
streamlit~=1.33.0
|
||||||
loguru~=0.7.2
|
loguru~=0.7.2
|
||||||
aiohttp~=3.9.3
|
aiohttp~=3.9.3
|
||||||
urllib3~=2.2.1
|
urllib3~=2.2.1
|
||||||
pillow~=9.5.0
|
pillow~=10.3.0
|
||||||
pydantic~=2.6.3
|
pydantic~=2.6.3
|
||||||
g4f~=0.2.5.4
|
g4f~=0.3.0.4
|
||||||
dashscope~=1.15.0
|
dashscope~=1.15.0
|
||||||
google.generativeai~=0.4.1
|
google.generativeai~=0.4.1
|
||||||
python-multipart~=0.0.9
|
python-multipart~=0.0.9
|
||||||
redis==5.0.3
|
redis==5.0.3
|
||||||
|
# if you use pillow~=10.3.0, you will get "PIL.Image' has no attribute 'ANTIALIAS'" error when resize video
|
||||||
|
# please install opencv-python to fix "PIL.Image' has no attribute 'ANTIALIAS'" error
|
||||||
|
opencv-python~=4.9.0.80
|
||||||
|
# for azure speech
|
||||||
|
# https://techcommunity.microsoft.com/t5/ai-azure-ai-services-blog/9-more-realistic-ai-voices-for-conversations-now-generally/ba-p/4099471
|
||||||
|
azure-cognitiveservices-speech~=1.37.0
|
||||||
|
git-changelog~=2.5.2
|
||||||
|
|||||||
BIN
resource/fonts/MicrosoftYaHeiBold.ttc
Normal file
BIN
resource/fonts/MicrosoftYaHeiNormal.ttc
Normal file
BIN
resource/fonts/UTM Kabel KT.ttf
Normal file
208
sites/docs/.vuepress/config.ts
Normal file
@@ -0,0 +1,208 @@
|
|||||||
|
import { viteBundler } from "@vuepress/bundler-vite";
|
||||||
|
import { defaultTheme } from "@vuepress/theme-default";
|
||||||
|
import { defineUserConfig } from "vuepress";
|
||||||
|
|
||||||
|
const base = "MoneyPrinterTurbo";
|
||||||
|
const isProd = process.env.NODE_ENV === "production";
|
||||||
|
|
||||||
|
export default defineUserConfig({
|
||||||
|
lang: "zh-CN",
|
||||||
|
base: `/${base}/`,
|
||||||
|
bundler: viteBundler(),
|
||||||
|
theme: defaultTheme({
|
||||||
|
repo: "harry0703/MoneyPrinterTurbo",
|
||||||
|
docsDir: "sites/docs",
|
||||||
|
colorModeSwitch: true,
|
||||||
|
locales: {
|
||||||
|
"/": {
|
||||||
|
// navbar
|
||||||
|
navbar: [
|
||||||
|
{ text: "Guide", link: "/guide/" },
|
||||||
|
// { text: "Components", link: "/components/" },
|
||||||
|
],
|
||||||
|
selectLanguageText: "Languages",
|
||||||
|
selectLanguageName: "English",
|
||||||
|
selectLanguageAriaLabel: "Select language",
|
||||||
|
// sidebar
|
||||||
|
sidebar: {
|
||||||
|
"/guide/": [
|
||||||
|
{
|
||||||
|
text: "Guide",
|
||||||
|
children: [
|
||||||
|
{ text: "Get Started", link: "/guide/README.md" },
|
||||||
|
{ text: "Video Demonstration", link: "/guide/video-demonstration.md" },
|
||||||
|
{ text: "Features", link: "/guide/features.md" },
|
||||||
|
{ text: "Speech Synthesis", link: "/guide/speech-synthesis.md" },
|
||||||
|
{ text: "Subtitle Generation", link: "/guide/subtitle-generation.md" },
|
||||||
|
{ text: "Background Music", link: "/guide/background-music.md" },
|
||||||
|
{ text: "Subtitle Font", link: "/guide/subtitle-font.md" },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: "Others",
|
||||||
|
children: [
|
||||||
|
{ text: "FAQ", link: "/guide/faq.md" },
|
||||||
|
{ text: "Feedback", link: "/guide/feedback.md" },
|
||||||
|
{ text: "Reference Project", link: "/guide/reference-project.md" },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
// "/components/": getComponentsSidebar("Components", "Advanced"),
|
||||||
|
},
|
||||||
|
// page meta
|
||||||
|
editLinkText: "Edit this page on GitHub",
|
||||||
|
},
|
||||||
|
"/zh/": {
|
||||||
|
// navbar
|
||||||
|
navbar: [
|
||||||
|
{ text: "指南", link: "/zh/guide/" },
|
||||||
|
// { text: "组件", link: "/zh/components/" },
|
||||||
|
],
|
||||||
|
selectLanguageText: "选择语言",
|
||||||
|
selectLanguageName: "简体中文",
|
||||||
|
selectLanguageAriaLabel: "选择语言",
|
||||||
|
// sidebar
|
||||||
|
sidebar: {
|
||||||
|
"/zh/guide/": [
|
||||||
|
{
|
||||||
|
text: "指南",
|
||||||
|
children: [
|
||||||
|
{ text: "快速开始", link: "/zh/guide/README.md" },
|
||||||
|
{ text: "配置要求", link: "/zh/guide/configuration-requirements.md" },
|
||||||
|
{ text: "视频演示", link: "/zh/guide/video-demonstration.md" },
|
||||||
|
{ text: "功能", link: "/zh/guide/features.md" },
|
||||||
|
{ text: "语音合成", link: "/zh/guide/speech-synthesis.md" },
|
||||||
|
{ text: "字幕生成", link: "/zh/guide/subtitle-generation.md" },
|
||||||
|
{ text: "背景音乐", link: "/zh/guide/background-music.md" },
|
||||||
|
{ text: "字幕字体", link: "/zh/guide/subtitle-font.md" },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: "其他",
|
||||||
|
children: [
|
||||||
|
{ text: "常见问题", link: "/zh/guide/faq.md" },
|
||||||
|
{ text: "反馈建议", link: "/zh/guide/feedback.md" },
|
||||||
|
{ text: "参考项目", link: "/zh/guide/reference-project.md" },
|
||||||
|
{ text: "特别感谢", link: "/zh/guide/special-thanks.md" },
|
||||||
|
{ text: "感谢赞助", link: "/zh/guide/thanks-for-sponsoring" },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
// "/zh/others/": getComponentsSidebar("组件", "高级"),
|
||||||
|
},
|
||||||
|
// page meta
|
||||||
|
editLinkText: "在 GitHub 上编辑此页",
|
||||||
|
lastUpdatedText: "上次更新",
|
||||||
|
contributorsText: "贡献者",
|
||||||
|
// custom containers
|
||||||
|
tip: "提示",
|
||||||
|
warning: "注意",
|
||||||
|
danger: "警告",
|
||||||
|
// 404 page
|
||||||
|
notFound: [
|
||||||
|
"这里什么都没有",
|
||||||
|
"我们怎么到这来了?",
|
||||||
|
"这是一个 404 页面",
|
||||||
|
"看起来我们进入了错误的链接",
|
||||||
|
],
|
||||||
|
backToHome: "返回首页",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
themePlugins: {
|
||||||
|
// only enable git plugin in production mode
|
||||||
|
git: isProd,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
locales: {
|
||||||
|
"/": {
|
||||||
|
lang: "en-US",
|
||||||
|
title: "MoneyPrinterTurbo",
|
||||||
|
description: "Generate short videos with one click using AI LLM.",
|
||||||
|
},
|
||||||
|
"/zh/": {
|
||||||
|
lang: "zh-CN",
|
||||||
|
title: "MoneyPrinterTurbo",
|
||||||
|
description: "利用AI大模型,一键生成高清短视频。",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
head: [
|
||||||
|
[
|
||||||
|
"link",
|
||||||
|
{
|
||||||
|
rel: "icon",
|
||||||
|
type: "image/png",
|
||||||
|
sizes: "16x16",
|
||||||
|
href: `/${base}/icons/favicon-16x16.png`,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"link",
|
||||||
|
{
|
||||||
|
rel: "icon",
|
||||||
|
type: "image/png",
|
||||||
|
sizes: "32x32",
|
||||||
|
href: `/${base}/icons/favicon-32x32.png`,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
["meta", { name: "application-name", content: "MoneyPrinterTurbo" }],
|
||||||
|
[
|
||||||
|
"meta",
|
||||||
|
{ name: "apple-mobile-web-app-title", content: "MoneyPrinterTurbo" },
|
||||||
|
],
|
||||||
|
["meta", { name: "apple-mobile-web-app-capable", content: "yes" }],
|
||||||
|
[
|
||||||
|
"meta",
|
||||||
|
{ name: "apple-mobile-web-app-status-bar-style", content: "black" },
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"link",
|
||||||
|
{
|
||||||
|
rel: "apple-touch-icon",
|
||||||
|
href: `/${base}/icons/apple-touch-icon-152x152.png`,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"link",
|
||||||
|
{
|
||||||
|
rel: "mask-icon",
|
||||||
|
href: "/${base}/icons/safari-pinned-tab.svg",
|
||||||
|
color: "#3eaf7c",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"meta",
|
||||||
|
{
|
||||||
|
name: "msapplication-TileImage",
|
||||||
|
content: "/${base}/icons/msapplication-icon-144x144.png",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
["meta", { name: "msapplication-TileColor", content: "#000000" }],
|
||||||
|
["meta", { name: "theme-color", content: "#3eaf7c" }],
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
function getGuideSidebar(groupA: string, groupB: string) {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
text: groupA,
|
||||||
|
children: ["README.md", { text: "特别感谢", link: "/zh/guide/special-thanks.md" }, "2.md"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: groupB,
|
||||||
|
children: ["custom-validator.md", "1.md", "2.md", "3.md"],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
function getComponentsSidebar(groupA: string, groupB: string) {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
text: groupA,
|
||||||
|
children: ["README.md", "1.md", "2.md"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: groupB,
|
||||||
|
children: ["custom-components.md"],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
BIN
sites/docs/.vuepress/public/api.jpg
Normal file
|
After Width: | Height: | Size: 252 KiB |
|
Before Width: | Height: | Size: 172 KiB After Width: | Height: | Size: 172 KiB |
BIN
sites/docs/.vuepress/public/hero.png
Normal file
|
After Width: | Height: | Size: 1.6 MiB |
BIN
sites/docs/.vuepress/public/icons/android-chrome-192x192.png
Normal file
|
After Width: | Height: | Size: 9.2 KiB |
BIN
sites/docs/.vuepress/public/icons/android-chrome-512x512.png
Normal file
|
After Width: | Height: | Size: 29 KiB |
BIN
sites/docs/.vuepress/public/icons/apple-touch-icon-120x120.png
Normal file
|
After Width: | Height: | Size: 3.3 KiB |
BIN
sites/docs/.vuepress/public/icons/apple-touch-icon-152x152.png
Normal file
|
After Width: | Height: | Size: 4.0 KiB |
BIN
sites/docs/.vuepress/public/icons/apple-touch-icon-180x180.png
Normal file
|
After Width: | Height: | Size: 4.6 KiB |
BIN
sites/docs/.vuepress/public/icons/apple-touch-icon-60x60.png
Normal file
|
After Width: | Height: | Size: 1.5 KiB |
BIN
sites/docs/.vuepress/public/icons/apple-touch-icon-76x76.png
Normal file
|
After Width: | Height: | Size: 1.8 KiB |
BIN
sites/docs/.vuepress/public/icons/apple-touch-icon.png
Normal file
|
After Width: | Height: | Size: 4.6 KiB |
BIN
sites/docs/.vuepress/public/icons/favicon-16x16.png
Normal file
|
After Width: | Height: | Size: 1.8 KiB |
BIN
sites/docs/.vuepress/public/icons/favicon-32x32.png
Normal file
|
After Width: | Height: | Size: 4.1 KiB |
BIN
sites/docs/.vuepress/public/icons/msapplication-icon-144x144.png
Normal file
|
After Width: | Height: | Size: 1.1 KiB |
BIN
sites/docs/.vuepress/public/icons/mstile-150x150.png
Normal file
|
After Width: | Height: | Size: 4.2 KiB |
149
sites/docs/.vuepress/public/icons/safari-pinned-tab.svg
Normal file
@@ -0,0 +1,149 @@
|
|||||||
|
<?xml version="1.0" standalone="no"?>
|
||||||
|
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN"
|
||||||
|
"http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
|
||||||
|
<svg version="1.0" xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="16.000000pt" height="16.000000pt" viewBox="0 0 16.000000 16.000000"
|
||||||
|
preserveAspectRatio="xMidYMid meet">
|
||||||
|
<metadata>
|
||||||
|
Created by potrace 1.11, written by Peter Selinger 2001-2013
|
||||||
|
</metadata>
|
||||||
|
<g transform="translate(0.000000,16.000000) scale(0.000320,-0.000320)"
|
||||||
|
fill="#000000" stroke="none">
|
||||||
|
<path d="M18 46618 c45 -75 122 -207 122 -211 0 -2 25 -45 55 -95 30 -50 55
|
||||||
|
-96 55 -102 0 -5 5 -10 10 -10 6 0 10 -4 10 -9 0 -5 73 -135 161 -288 89 -153
|
||||||
|
173 -298 187 -323 14 -25 32 -57 41 -72 88 -149 187 -324 189 -335 2 -7 8 -13
|
||||||
|
13 -13 5 0 9 -4 9 -10 0 -5 46 -89 103 -187 175 -302 490 -846 507 -876 8 -16
|
||||||
|
20 -36 25 -45 28 -46 290 -498 339 -585 13 -23 74 -129 136 -236 61 -107 123
|
||||||
|
-215 137 -240 14 -25 29 -50 33 -56 5 -5 23 -37 40 -70 18 -33 38 -67 44 -75
|
||||||
|
11 -16 21 -33 63 -109 14 -25 29 -50 33 -56 4 -5 21 -35 38 -65 55 -100 261
|
||||||
|
-455 269 -465 4 -5 14 -21 20 -35 15 -29 41 -75 103 -180 24 -41 52 -88 60
|
||||||
|
-105 9 -16 57 -100 107 -185 112 -193 362 -626 380 -660 8 -14 23 -38 33 -55
|
||||||
|
11 -16 23 -37 27 -45 4 -8 26 -46 48 -85 23 -38 53 -90 67 -115 46 -81 64
|
||||||
|
-113 178 -310 62 -107 121 -210 132 -227 37 -67 56 -99 85 -148 16 -27 32 -57
|
||||||
|
36 -65 4 -8 15 -27 25 -42 9 -15 53 -89 96 -165 44 -76 177 -307 296 -513 120
|
||||||
|
-206 268 -463 330 -570 131 -227 117 -203 200 -348 36 -62 73 -125 82 -140 10
|
||||||
|
-15 21 -34 25 -42 4 -8 20 -37 36 -65 17 -27 38 -65 48 -82 49 -85 64 -111 87
|
||||||
|
-153 13 -25 28 -49 32 -55 4 -5 78 -134 165 -285 87 -151 166 -288 176 -305
|
||||||
|
10 -16 26 -43 35 -59 9 -17 125 -217 257 -445 132 -229 253 -441 270 -471 17
|
||||||
|
-30 45 -79 64 -108 18 -29 33 -54 33 -57 0 -2 20 -37 44 -77 24 -40 123 -212
|
||||||
|
221 -383 97 -170 190 -330 205 -355 16 -25 39 -65 53 -90 13 -25 81 -144 152
|
||||||
|
-265 70 -121 137 -238 150 -260 12 -22 37 -65 55 -95 18 -30 43 -73 55 -95 12
|
||||||
|
-22 48 -85 80 -140 77 -132 163 -280 190 -330 13 -22 71 -123 130 -225 59
|
||||||
|
-102 116 -199 126 -217 10 -17 29 -50 43 -72 15 -22 26 -43 26 -45 0 -2 27
|
||||||
|
-50 60 -106 33 -56 60 -103 60 -105 0 -2 55 -98 90 -155 8 -14 182 -316 239
|
||||||
|
-414 13 -22 45 -79 72 -124 27 -46 49 -86 49 -89 0 -2 14 -24 30 -48 16 -24
|
||||||
|
30 -46 30 -49 0 -5 74 -135 100 -176 5 -8 24 -42 43 -75 50 -88 58 -101 262
|
||||||
|
-455 104 -179 199 -345 213 -370 14 -25 28 -49 32 -55 4 -5 17 -26 28 -45 10
|
||||||
|
-19 62 -109 114 -200 114 -197 133 -230 170 -295 16 -27 33 -57 38 -65 17 -28
|
||||||
|
96 -165 103 -180 4 -8 16 -28 26 -45 10 -16 77 -131 148 -255 72 -124 181
|
||||||
|
-313 243 -420 62 -107 121 -209 131 -227 35 -62 323 -560 392 -678 38 -66 83
|
||||||
|
-145 100 -175 16 -30 33 -59 37 -65 4 -5 17 -27 29 -47 34 -61 56 -100 90
|
||||||
|
-156 17 -29 31 -55 31 -57 0 -2 17 -32 39 -67 21 -35 134 -229 251 -433 117
|
||||||
|
-203 235 -407 261 -451 27 -45 49 -85 49 -88 0 -4 8 -19 19 -34 15 -21 200
|
||||||
|
-341 309 -533 10 -19 33 -58 51 -87 17 -29 31 -54 31 -56 0 -2 25 -44 55 -94
|
||||||
|
30 -50 55 -95 55 -98 0 -4 6 -15 14 -23 7 -9 27 -41 43 -71 17 -30 170 -297
|
||||||
|
342 -594 171 -296 311 -542 311 -547 0 -5 5 -9 10 -9 6 0 10 -4 10 -10 0 -5
|
||||||
|
22 -47 49 -92 27 -46 58 -99 68 -118 24 -43 81 -140 93 -160 5 -8 66 -114 135
|
||||||
|
-235 69 -121 130 -227 135 -235 12 -21 259 -447 283 -490 10 -19 28 -47 38
|
||||||
|
-62 11 -14 19 -29 19 -32 0 -3 37 -69 83 -148 99 -170 305 -526 337 -583 13
|
||||||
|
-22 31 -53 41 -70 11 -16 22 -37 26 -45 7 -14 82 -146 103 -180 14 -24 181
|
||||||
|
-311 205 -355 13 -22 46 -80 75 -130 29 -49 64 -110 78 -135 14 -25 51 -88 82
|
||||||
|
-140 31 -52 59 -102 63 -110 4 -8 18 -33 31 -55 205 -353 284 -489 309 -535
|
||||||
|
17 -30 45 -78 62 -106 18 -28 36 -60 39 -72 4 -12 12 -22 17 -22 5 0 9 -4 9
|
||||||
|
-10 0 -5 109 -197 241 -427 133 -230 250 -431 259 -448 51 -90 222 -385 280
|
||||||
|
-485 37 -63 78 -135 92 -160 14 -25 67 -117 118 -205 51 -88 101 -175 111
|
||||||
|
-193 34 -58 55 -95 149 -257 51 -88 101 -173 110 -190 9 -16 76 -131 147 -255
|
||||||
|
72 -124 140 -241 151 -260 61 -108 281 -489 355 -615 38 -66 77 -133 87 -150
|
||||||
|
35 -63 91 -161 100 -175 14 -23 99 -169 128 -220 54 -97 135 -235 142 -245 4
|
||||||
|
-5 20 -32 35 -60 26 -48 238 -416 276 -480 10 -16 26 -46 37 -65 30 -53 382
|
||||||
|
-661 403 -695 10 -16 22 -37 26 -45 4 -8 26 -48 50 -88 24 -41 43 -75 43 -77
|
||||||
|
0 -2 22 -40 50 -85 27 -45 50 -84 50 -86 0 -3 38 -69 83 -147 84 -142 302
|
||||||
|
-520 340 -587 10 -19 34 -60 52 -90 18 -30 44 -75 57 -100 14 -25 45 -79 70
|
||||||
|
-120 25 -41 56 -96 70 -121 14 -25 77 -133 138 -240 62 -107 122 -210 132
|
||||||
|
-229 25 -43 310 -535 337 -581 11 -19 26 -45 34 -59 17 -32 238 -414 266 -460
|
||||||
|
11 -19 24 -41 28 -49 3 -7 75 -133 160 -278 84 -146 153 -269 153 -274 0 -5 5
|
||||||
|
-9 10 -9 6 0 10 -4 10 -10 0 -5 82 -150 181 -322 182 -314 201 -346 240 -415
|
||||||
|
12 -21 80 -139 152 -263 71 -124 141 -245 155 -270 14 -25 28 -49 32 -55 6 -8
|
||||||
|
145 -248 220 -380 37 -66 209 -362 229 -395 11 -19 24 -42 28 -49 4 -8 67
|
||||||
|
-118 140 -243 73 -125 133 -230 133 -233 0 -2 15 -28 33 -57 19 -29 47 -78 64
|
||||||
|
-108 17 -30 53 -93 79 -139 53 -90 82 -141 157 -272 82 -142 115 -199 381
|
||||||
|
-659 142 -245 268 -463 281 -485 12 -22 71 -125 132 -230 60 -104 172 -298
|
||||||
|
248 -430 76 -132 146 -253 156 -270 11 -16 22 -36 26 -44 3 -8 30 -54 60 -103
|
||||||
|
29 -49 53 -91 53 -93 0 -3 18 -34 40 -70 22 -36 40 -67 40 -69 0 -2 37 -66 81
|
||||||
|
-142 45 -77 98 -168 119 -204 20 -36 47 -81 58 -100 12 -19 27 -47 33 -62 6
|
||||||
|
-16 15 -28 20 -28 5 0 9 -4 9 -9 0 -6 63 -118 140 -251 77 -133 140 -243 140
|
||||||
|
-245 0 -2 18 -33 41 -70 22 -37 49 -83 60 -101 10 -19 29 -51 40 -71 25 -45
|
||||||
|
109 -189 126 -218 7 -11 17 -29 22 -40 6 -11 22 -38 35 -60 14 -22 37 -62 52
|
||||||
|
-90 14 -27 35 -62 45 -77 11 -14 19 -29 19 -32 0 -3 18 -35 40 -71 22 -36 40
|
||||||
|
-67 40 -69 0 -2 19 -35 42 -72 23 -38 55 -94 72 -124 26 -47 139 -244 171
|
||||||
|
-298 6 -9 21 -36 34 -60 28 -48 37 -51 51 -19 6 12 19 36 29 52 10 17 27 46
|
||||||
|
38 65 11 19 104 181 208 360 103 179 199 345 213 370 14 25 42 74 64 109 21
|
||||||
|
34 38 65 38 67 0 2 18 33 40 69 22 36 40 67 40 69 0 3 177 310 199 346 16 26
|
||||||
|
136 234 140 244 2 5 25 44 52 88 27 44 49 81 49 84 0 2 18 34 40 70 22 36 40
|
||||||
|
67 40 69 0 2 20 36 43 77 35 58 169 289 297 513 9 17 50 86 90 155 40 69 86
|
||||||
|
150 103 180 16 30 35 62 41 70 6 8 16 24 22 35 35 64 72 129 167 293 59 100
|
||||||
|
116 199 127 220 11 20 30 53 41 72 43 72 1070 1850 1121 1940 14 25 65 113
|
||||||
|
113 195 48 83 96 166 107 185 10 19 28 50 38 68 11 18 73 124 137 235 64 111
|
||||||
|
175 303 246 427 71 124 173 299 225 390 52 91 116 202 143 248 27 45 49 85 49
|
||||||
|
89 0 4 6 14 14 22 7 9 28 43 46 76 26 47 251 436 378 655 11 19 29 51 40 70
|
||||||
|
11 19 101 176 201 348 99 172 181 317 181 323 0 5 5 9 10 9 6 0 10 5 10 11 0
|
||||||
|
6 8 23 18 37 11 15 32 52 49 82 16 30 130 228 253 440 122 212 234 405 248
|
||||||
|
430 13 25 39 70 57 100 39 65 69 117 130 225 25 44 50 87 55 95 12 19 78 134
|
||||||
|
220 380 61 107 129 224 150 260 161 277 222 382 246 425 15 28 47 83 71 123
|
||||||
|
24 41 43 78 43 83 0 5 4 9 8 9 4 0 13 12 19 28 7 15 23 45 36 67 66 110 277
|
||||||
|
478 277 483 0 3 6 13 14 21 7 9 27 41 43 71 17 30 45 80 63 110 34 57 375 649
|
||||||
|
394 685 6 11 16 27 22 35 6 8 26 42 44 75 18 33 41 74 51 90 10 17 24 41 32
|
||||||
|
55 54 97 72 128 88 152 11 14 19 28 19 30 0 3 79 141 175 308 96 167 175 305
|
||||||
|
175 308 0 3 6 13 14 21 7 9 26 39 41 66 33 60 276 483 338 587 24 40 46 80 50
|
||||||
|
88 4 8 13 24 20 35 14 23 95 163 125 215 11 19 52 91 92 160 40 69 80 139 90
|
||||||
|
155 9 17 103 179 207 360 105 182 200 346 211 365 103 181 463 802 489 845 7
|
||||||
|
11 15 27 19 35 4 8 29 51 55 95 64 110 828 1433 848 1470 9 17 24 41 33 55 9
|
||||||
|
14 29 48 45 77 15 28 52 93 82 145 30 51 62 107 71 123 17 30 231 398 400 690
|
||||||
|
51 88 103 179 115 202 12 23 26 48 32 55 6 7 24 38 40 68 17 30 61 107 98 170
|
||||||
|
37 63 84 144 103 180 19 36 41 72 48 81 8 8 14 18 14 21 0 4 27 51 59 106 32
|
||||||
|
55 72 124 89 154 16 29 71 125 122 213 51 88 104 180 118 205 13 25 28 50 32
|
||||||
|
55 4 6 17 26 28 45 11 19 45 80 77 135 31 55 66 116 77 135 11 19 88 152 171
|
||||||
|
295 401 694 620 1072 650 1125 11 19 87 152 170 295 83 143 158 273 166 288 9
|
||||||
|
16 21 36 26 45 6 9 31 52 55 96 25 43 54 94 66 115 11 20 95 164 186 321 91
|
||||||
|
157 173 299 182 315 9 17 26 46 37 65 12 19 66 114 121 210 56 96 108 186 117
|
||||||
|
200 8 14 24 40 34 59 24 45 383 664 412 713 5 9 17 29 26 45 15 28 120 210
|
||||||
|
241 419 36 61 68 117 72 125 4 8 12 23 19 34 35 57 245 420 262 453 11 20 35
|
||||||
|
61 53 90 17 29 32 54 32 56 0 3 28 51 62 108 33 57 70 119 80 138 10 19 23 42
|
||||||
|
28 50 5 8 32 53 59 100 27 47 149 258 271 470 122 212 234 405 248 430 30 53
|
||||||
|
62 108 80 135 6 11 15 27 19 35 4 8 85 150 181 315 96 165 187 323 202 350 31
|
||||||
|
56 116 202 130 225 5 8 25 42 43 75 19 33 92 159 162 280 149 257 157 271 202
|
||||||
|
350 19 33 38 67 43 75 9 14 228 392 275 475 12 22 55 96 95 165 40 69 80 139
|
||||||
|
90 155 24 42 202 350 221 383 9 15 27 47 41 72 14 25 75 131 136 236 61 106
|
||||||
|
121 210 134 232 99 172 271 470 279 482 5 8 23 40 40 70 18 30 81 141 142 245
|
||||||
|
60 105 121 210 135 235 14 25 71 124 127 220 56 96 143 247 194 335 51 88 96
|
||||||
|
167 102 175 14 24 180 311 204 355 23 43 340 590 356 615 5 8 50 87 101 175
|
||||||
|
171 301 517 898 582 1008 25 43 46 81 46 83 0 2 12 23 27 47 14 23 40 67 56
|
||||||
|
97 16 30 35 62 42 70 7 8 15 22 18 30 4 8 20 38 37 65 16 28 33 57 37 65 6 12
|
||||||
|
111 196 143 250 5 8 55 95 112 193 57 98 113 195 126 215 12 20 27 46 32 57 6
|
||||||
|
11 14 27 20 35 5 8 76 130 156 270 80 140 165 287 187 325 23 39 52 90 66 115
|
||||||
|
13 25 30 52 37 61 8 8 14 18 14 21 0 4 41 77 92 165 50 87 175 302 276 478
|
||||||
|
101 176 208 360 236 408 28 49 67 117 86 152 19 35 41 70 48 77 6 6 12 15 12
|
||||||
|
19 0 7 124 224 167 291 12 21 23 40 23 42 0 2 21 40 46 83 26 43 55 92 64 109
|
||||||
|
54 95 327 568 354 614 19 30 45 75 59 100 71 128 82 145 89 148 4 2 8 8 8 13
|
||||||
|
0 5 42 82 94 172 311 538 496 858 518 897 14 25 40 70 58 100 18 30 42 71 53
|
||||||
|
90 10 19 79 139 152 265 73 127 142 246 153 265 10 19 43 76 72 125 29 50 63
|
||||||
|
108 75 130 65 116 80 140 87 143 4 2 8 8 8 12 0 8 114 212 140 250 6 8 14 24
|
||||||
|
20 35 5 11 54 97 108 190 l100 170 -9611 3 c-5286 1 -9614 -1 -9618 -5 -5 -6
|
||||||
|
-419 -719 -619 -1068 -89 -155 -267 -463 -323 -560 -38 -66 -81 -140 -95 -165
|
||||||
|
-31 -56 -263 -457 -526 -910 -110 -190 -224 -388 -254 -440 -29 -52 -61 -109
|
||||||
|
-71 -125 -23 -39 -243 -420 -268 -465 -11 -19 -204 -352 -428 -740 -224 -388
|
||||||
|
-477 -826 -563 -975 -85 -148 -185 -322 -222 -385 -37 -63 -120 -207 -185
|
||||||
|
-320 -65 -113 -177 -306 -248 -430 -72 -124 -172 -297 -222 -385 -51 -88 -142
|
||||||
|
-245 -202 -350 -131 -226 -247 -427 -408 -705 -65 -113 -249 -432 -410 -710
|
||||||
|
-160 -278 -388 -673 -506 -877 -118 -205 -216 -373 -219 -373 -3 0 -52 82
|
||||||
|
-109 183 -58 100 -144 250 -192 332 -95 164 -402 696 -647 1120 -85 149 -228
|
||||||
|
396 -317 550 -212 365 -982 1700 -1008 1745 -10 19 -43 76 -72 125 -29 50 -64
|
||||||
|
110 -77 135 -14 25 -63 110 -110 190 -47 80 -96 165 -110 190 -14 25 -99 171
|
||||||
|
-188 325 -89 154 -174 300 -188 325 -13 25 -64 113 -112 195 -48 83 -140 242
|
||||||
|
-205 355 -65 113 -183 317 -263 454 -79 137 -152 264 -163 282 -50 89 -335
|
||||||
|
583 -354 614 -12 19 -34 58 -50 85 -15 28 -129 226 -253 440 -124 215 -235
|
||||||
|
408 -247 430 -12 22 -69 121 -127 220 -58 99 -226 389 -373 645 -148 256 -324
|
||||||
|
561 -392 678 -67 117 -134 232 -147 255 -13 23 -33 59 -46 80 l-22 37 -9615 0
|
||||||
|
-9615 0 20 -32z"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 10 KiB |
BIN
sites/docs/.vuepress/public/logo.png
Normal file
|
After Width: | Height: | Size: 1.6 MiB |
21
sites/docs/.vuepress/public/manifest.webmanifest
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
{
|
||||||
|
"name": "VuePress",
|
||||||
|
"short_name": "VuePress",
|
||||||
|
"description": "Vue-powered Static Site Generator",
|
||||||
|
"start_url": "/index.html",
|
||||||
|
"display": "standalone",
|
||||||
|
"background_color": "#fff",
|
||||||
|
"theme_color": "#3eaf7c",
|
||||||
|
"icons": [
|
||||||
|
{
|
||||||
|
"src": "/icons/android-chrome-192x192.png",
|
||||||
|
"sizes": "192x192",
|
||||||
|
"type": "image/png"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"src": "/icons/android-chrome-512x512.png",
|
||||||
|
"sizes": "512x512",
|
||||||
|
"type": "image/png"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
BIN
sites/docs/.vuepress/public/picwish.jpg
Normal file
|
After Width: | Height: | Size: 178 KiB |
BIN
sites/docs/.vuepress/public/reccloud.cn.jpg
Normal file
|
After Width: | Height: | Size: 294 KiB |
BIN
sites/docs/.vuepress/public/reccloud.com.jpg
Normal file
|
After Width: | Height: | Size: 256 KiB |
|
Before Width: | Height: | Size: 100 KiB After Width: | Height: | Size: 100 KiB |
941
sites/docs/.vuepress/public/voice-list.txt
Normal file
@@ -0,0 +1,941 @@
|
|||||||
|
Name: af-ZA-AdriNeural
|
||||||
|
Gender: Female
|
||||||
|
|
||||||
|
Name: af-ZA-WillemNeural
|
||||||
|
Gender: Male
|
||||||
|
|
||||||
|
Name: am-ET-AmehaNeural
|
||||||
|
Gender: Male
|
||||||
|
|
||||||
|
Name: am-ET-MekdesNeural
|
||||||
|
Gender: Female
|
||||||
|
|
||||||
|
Name: ar-AE-FatimaNeural
|
||||||
|
Gender: Female
|
||||||
|
|
||||||
|
Name: ar-AE-HamdanNeural
|
||||||
|
Gender: Male
|
||||||
|
|
||||||
|
Name: ar-BH-AliNeural
|
||||||
|
Gender: Male
|
||||||
|
|
||||||
|
Name: ar-BH-LailaNeural
|
||||||
|
Gender: Female
|
||||||
|
|
||||||
|
Name: ar-DZ-AminaNeural
|
||||||
|
Gender: Female
|
||||||
|
|
||||||
|
Name: ar-DZ-IsmaelNeural
|
||||||
|
Gender: Male
|
||||||
|
|
||||||
|
Name: ar-EG-SalmaNeural
|
||||||
|
Gender: Female
|
||||||
|
|
||||||
|
Name: ar-EG-ShakirNeural
|
||||||
|
Gender: Male
|
||||||
|
|
||||||
|
Name: ar-IQ-BasselNeural
|
||||||
|
Gender: Male
|
||||||
|
|
||||||
|
Name: ar-IQ-RanaNeural
|
||||||
|
Gender: Female
|
||||||
|
|
||||||
|
Name: ar-JO-SanaNeural
|
||||||
|
Gender: Female
|
||||||
|
|
||||||
|
Name: ar-JO-TaimNeural
|
||||||
|
Gender: Male
|
||||||
|
|
||||||
|
Name: ar-KW-FahedNeural
|
||||||
|
Gender: Male
|
||||||
|
|
||||||
|
Name: ar-KW-NouraNeural
|
||||||
|
Gender: Female
|
||||||
|
|
||||||
|
Name: ar-LB-LaylaNeural
|
||||||
|
Gender: Female
|
||||||
|
|
||||||
|
Name: ar-LB-RamiNeural
|
||||||
|
Gender: Male
|
||||||
|
|
||||||
|
Name: ar-LY-ImanNeural
|
||||||
|
Gender: Female
|
||||||
|
|
||||||
|
Name: ar-LY-OmarNeural
|
||||||
|
Gender: Male
|
||||||
|
|
||||||
|
Name: ar-MA-JamalNeural
|
||||||
|
Gender: Male
|
||||||
|
|
||||||
|
Name: ar-MA-MounaNeural
|
||||||
|
Gender: Female
|
||||||
|
|
||||||
|
Name: ar-OM-AbdullahNeural
|
||||||
|
Gender: Male
|
||||||
|
|
||||||
|
Name: ar-OM-AyshaNeural
|
||||||
|
Gender: Female
|
||||||
|
|
||||||
|
Name: ar-QA-AmalNeural
|
||||||
|
Gender: Female
|
||||||
|
|
||||||
|
Name: ar-QA-MoazNeural
|
||||||
|
Gender: Male
|
||||||
|
|
||||||
|
Name: ar-SA-HamedNeural
|
||||||
|
Gender: Male
|
||||||
|
|
||||||
|
Name: ar-SA-ZariyahNeural
|
||||||
|
Gender: Female
|
||||||
|
|
||||||
|
Name: ar-SY-AmanyNeural
|
||||||
|
Gender: Female
|
||||||
|
|
||||||
|
Name: ar-SY-LaithNeural
|
||||||
|
Gender: Male
|
||||||
|
|
||||||
|
Name: ar-TN-HediNeural
|
||||||
|
Gender: Male
|
||||||
|
|
||||||
|
Name: ar-TN-ReemNeural
|
||||||
|
Gender: Female
|
||||||
|
|
||||||
|
Name: ar-YE-MaryamNeural
|
||||||
|
Gender: Female
|
||||||
|
|
||||||
|
Name: ar-YE-SalehNeural
|
||||||
|
Gender: Male
|
||||||
|
|
||||||
|
Name: az-AZ-BabekNeural
|
||||||
|
Gender: Male
|
||||||
|
|
||||||
|
Name: az-AZ-BanuNeural
|
||||||
|
Gender: Female
|
||||||
|
|
||||||
|
Name: bg-BG-BorislavNeural
|
||||||
|
Gender: Male
|
||||||
|
|
||||||
|
Name: bg-BG-KalinaNeural
|
||||||
|
Gender: Female
|
||||||
|
|
||||||
|
Name: bn-BD-NabanitaNeural
|
||||||
|
Gender: Female
|
||||||
|
|
||||||
|
Name: bn-BD-PradeepNeural
|
||||||
|
Gender: Male
|
||||||
|
|
||||||
|
Name: bn-IN-BashkarNeural
|
||||||
|
Gender: Male
|
||||||
|
|
||||||
|
Name: bn-IN-TanishaaNeural
|
||||||
|
Gender: Female
|
||||||
|
|
||||||
|
Name: bs-BA-GoranNeural
|
||||||
|
Gender: Male
|
||||||
|
|
||||||
|
Name: bs-BA-VesnaNeural
|
||||||
|
Gender: Female
|
||||||
|
|
||||||
|
Name: ca-ES-EnricNeural
|
||||||
|
Gender: Male
|
||||||
|
|
||||||
|
Name: ca-ES-JoanaNeural
|
||||||
|
Gender: Female
|
||||||
|
|
||||||
|
Name: cs-CZ-AntoninNeural
|
||||||
|
Gender: Male
|
||||||
|
|
||||||
|
Name: cs-CZ-VlastaNeural
|
||||||
|
Gender: Female
|
||||||
|
|
||||||
|
Name: cy-GB-AledNeural
|
||||||
|
Gender: Male
|
||||||
|
|
||||||
|
Name: cy-GB-NiaNeural
|
||||||
|
Gender: Female
|
||||||
|
|
||||||
|
Name: da-DK-ChristelNeural
|
||||||
|
Gender: Female
|
||||||
|
|
||||||
|
Name: da-DK-JeppeNeural
|
||||||
|
Gender: Male
|
||||||
|
|
||||||
|
Name: de-AT-IngridNeural
|
||||||
|
Gender: Female
|
||||||
|
|
||||||
|
Name: de-AT-JonasNeural
|
||||||
|
Gender: Male
|
||||||
|
|
||||||
|
Name: de-CH-JanNeural
|
||||||
|
Gender: Male
|
||||||
|
|
||||||
|
Name: de-CH-LeniNeural
|
||||||
|
Gender: Female
|
||||||
|
|
||||||
|
Name: de-DE-AmalaNeural
|
||||||
|
Gender: Female
|
||||||
|
|
||||||
|
Name: de-DE-ConradNeural
|
||||||
|
Gender: Male
|
||||||
|
|
||||||
|
Name: de-DE-FlorianMultilingualNeural
|
||||||
|
Gender: Male
|
||||||
|
|
||||||
|
Name: de-DE-KatjaNeural
|
||||||
|
Gender: Female
|
||||||
|
|
||||||
|
Name: de-DE-KillianNeural
|
||||||
|
Gender: Male
|
||||||
|
|
||||||
|
Name: de-DE-SeraphinaMultilingualNeural
|
||||||
|
Gender: Female
|
||||||
|
|
||||||
|
Name: el-GR-AthinaNeural
|
||||||
|
Gender: Female
|
||||||
|
|
||||||
|
Name: el-GR-NestorasNeural
|
||||||
|
Gender: Male
|
||||||
|
|
||||||
|
Name: en-AU-NatashaNeural
|
||||||
|
Gender: Female
|
||||||
|
|
||||||
|
Name: en-AU-WilliamNeural
|
||||||
|
Gender: Male
|
||||||
|
|
||||||
|
Name: en-CA-ClaraNeural
|
||||||
|
Gender: Female
|
||||||
|
|
||||||
|
Name: en-CA-LiamNeural
|
||||||
|
Gender: Male
|
||||||
|
|
||||||
|
Name: en-GB-LibbyNeural
|
||||||
|
Gender: Female
|
||||||
|
|
||||||
|
Name: en-GB-MaisieNeural
|
||||||
|
Gender: Female
|
||||||
|
|
||||||
|
Name: en-GB-RyanNeural
|
||||||
|
Gender: Male
|
||||||
|
|
||||||
|
Name: en-GB-SoniaNeural
|
||||||
|
Gender: Female
|
||||||
|
|
||||||
|
Name: en-GB-ThomasNeural
|
||||||
|
Gender: Male
|
||||||
|
|
||||||
|
Name: en-HK-SamNeural
|
||||||
|
Gender: Male
|
||||||
|
|
||||||
|
Name: en-HK-YanNeural
|
||||||
|
Gender: Female
|
||||||
|
|
||||||
|
Name: en-IE-ConnorNeural
|
||||||
|
Gender: Male
|
||||||
|
|
||||||
|
Name: en-IE-EmilyNeural
|
||||||
|
Gender: Female
|
||||||
|
|
||||||
|
Name: en-IN-NeerjaExpressiveNeural
|
||||||
|
Gender: Female
|
||||||
|
|
||||||
|
Name: en-IN-NeerjaNeural
|
||||||
|
Gender: Female
|
||||||
|
|
||||||
|
Name: en-IN-PrabhatNeural
|
||||||
|
Gender: Male
|
||||||
|
|
||||||
|
Name: en-KE-AsiliaNeural
|
||||||
|
Gender: Female
|
||||||
|
|
||||||
|
Name: en-KE-ChilembaNeural
|
||||||
|
Gender: Male
|
||||||
|
|
||||||
|
Name: en-NG-AbeoNeural
|
||||||
|
Gender: Male
|
||||||
|
|
||||||
|
Name: en-NG-EzinneNeural
|
||||||
|
Gender: Female
|
||||||
|
|
||||||
|
Name: en-NZ-MitchellNeural
|
||||||
|
Gender: Male
|
||||||
|
|
||||||
|
Name: en-NZ-MollyNeural
|
||||||
|
Gender: Female
|
||||||
|
|
||||||
|
Name: en-PH-JamesNeural
|
||||||
|
Gender: Male
|
||||||
|
|
||||||
|
Name: en-PH-RosaNeural
|
||||||
|
Gender: Female
|
||||||
|
|
||||||
|
Name: en-SG-LunaNeural
|
||||||
|
Gender: Female
|
||||||
|
|
||||||
|
Name: en-SG-WayneNeural
|
||||||
|
Gender: Male
|
||||||
|
|
||||||
|
Name: en-TZ-ElimuNeural
|
||||||
|
Gender: Male
|
||||||
|
|
||||||
|
Name: en-TZ-ImaniNeural
|
||||||
|
Gender: Female
|
||||||
|
|
||||||
|
Name: en-US-AnaNeural
|
||||||
|
Gender: Female
|
||||||
|
|
||||||
|
Name: en-US-AndrewNeural
|
||||||
|
Gender: Male
|
||||||
|
|
||||||
|
Name: en-US-AriaNeural
|
||||||
|
Gender: Female
|
||||||
|
|
||||||
|
Name: en-US-AvaNeural
|
||||||
|
Gender: Female
|
||||||
|
|
||||||
|
Name: en-US-BrianNeural
|
||||||
|
Gender: Male
|
||||||
|
|
||||||
|
Name: en-US-ChristopherNeural
|
||||||
|
Gender: Male
|
||||||
|
|
||||||
|
Name: en-US-EmmaNeural
|
||||||
|
Gender: Female
|
||||||
|
|
||||||
|
Name: en-US-EricNeural
|
||||||
|
Gender: Male
|
||||||
|
|
||||||
|
Name: en-US-GuyNeural
|
||||||
|
Gender: Male
|
||||||
|
|
||||||
|
Name: en-US-JennyNeural
|
||||||
|
Gender: Female
|
||||||
|
|
||||||
|
Name: en-US-MichelleNeural
|
||||||
|
Gender: Female
|
||||||
|
|
||||||
|
Name: en-US-RogerNeural
|
||||||
|
Gender: Male
|
||||||
|
|
||||||
|
Name: en-US-SteffanNeural
|
||||||
|
Gender: Male
|
||||||
|
|
||||||
|
Name: en-ZA-LeahNeural
|
||||||
|
Gender: Female
|
||||||
|
|
||||||
|
Name: en-ZA-LukeNeural
|
||||||
|
Gender: Male
|
||||||
|
|
||||||
|
Name: es-AR-ElenaNeural
|
||||||
|
Gender: Female
|
||||||
|
|
||||||
|
Name: es-AR-TomasNeural
|
||||||
|
Gender: Male
|
||||||
|
|
||||||
|
Name: es-BO-MarceloNeural
|
||||||
|
Gender: Male
|
||||||
|
|
||||||
|
Name: es-BO-SofiaNeural
|
||||||
|
Gender: Female
|
||||||
|
|
||||||
|
Name: es-CL-CatalinaNeural
|
||||||
|
Gender: Female
|
||||||
|
|
||||||
|
Name: es-CL-LorenzoNeural
|
||||||
|
Gender: Male
|
||||||
|
|
||||||
|
Name: es-CO-GonzaloNeural
|
||||||
|
Gender: Male
|
||||||
|
|
||||||
|
Name: es-CO-SalomeNeural
|
||||||
|
Gender: Female
|
||||||
|
|
||||||
|
Name: es-CR-JuanNeural
|
||||||
|
Gender: Male
|
||||||
|
|
||||||
|
Name: es-CR-MariaNeural
|
||||||
|
Gender: Female
|
||||||
|
|
||||||
|
Name: es-CU-BelkysNeural
|
||||||
|
Gender: Female
|
||||||
|
|
||||||
|
Name: es-CU-ManuelNeural
|
||||||
|
Gender: Male
|
||||||
|
|
||||||
|
Name: es-DO-EmilioNeural
|
||||||
|
Gender: Male
|
||||||
|
|
||||||
|
Name: es-DO-RamonaNeural
|
||||||
|
Gender: Female
|
||||||
|
|
||||||
|
Name: es-EC-AndreaNeural
|
||||||
|
Gender: Female
|
||||||
|
|
||||||
|
Name: es-EC-LuisNeural
|
||||||
|
Gender: Male
|
||||||
|
|
||||||
|
Name: es-ES-AlvaroNeural
|
||||||
|
Gender: Male
|
||||||
|
|
||||||
|
Name: es-ES-ElviraNeural
|
||||||
|
Gender: Female
|
||||||
|
|
||||||
|
Name: es-ES-XimenaNeural
|
||||||
|
Gender: Female
|
||||||
|
|
||||||
|
Name: es-GQ-JavierNeural
|
||||||
|
Gender: Male
|
||||||
|
|
||||||
|
Name: es-GQ-TeresaNeural
|
||||||
|
Gender: Female
|
||||||
|
|
||||||
|
Name: es-GT-AndresNeural
|
||||||
|
Gender: Male
|
||||||
|
|
||||||
|
Name: es-GT-MartaNeural
|
||||||
|
Gender: Female
|
||||||
|
|
||||||
|
Name: es-HN-CarlosNeural
|
||||||
|
Gender: Male
|
||||||
|
|
||||||
|
Name: es-HN-KarlaNeural
|
||||||
|
Gender: Female
|
||||||
|
|
||||||
|
Name: es-MX-DaliaNeural
|
||||||
|
Gender: Female
|
||||||
|
|
||||||
|
Name: es-MX-JorgeNeural
|
||||||
|
Gender: Male
|
||||||
|
|
||||||
|
Name: es-NI-FedericoNeural
|
||||||
|
Gender: Male
|
||||||
|
|
||||||
|
Name: es-NI-YolandaNeural
|
||||||
|
Gender: Female
|
||||||
|
|
||||||
|
Name: es-PA-MargaritaNeural
|
||||||
|
Gender: Female
|
||||||
|
|
||||||
|
Name: es-PA-RobertoNeural
|
||||||
|
Gender: Male
|
||||||
|
|
||||||
|
Name: es-PE-AlexNeural
|
||||||
|
Gender: Male
|
||||||
|
|
||||||
|
Name: es-PE-CamilaNeural
|
||||||
|
Gender: Female
|
||||||
|
|
||||||
|
Name: es-PR-KarinaNeural
|
||||||
|
Gender: Female
|
||||||
|
|
||||||
|
Name: es-PR-VictorNeural
|
||||||
|
Gender: Male
|
||||||
|
|
||||||
|
Name: es-PY-MarioNeural
|
||||||
|
Gender: Male
|
||||||
|
|
||||||
|
Name: es-PY-TaniaNeural
|
||||||
|
Gender: Female
|
||||||
|
|
||||||
|
Name: es-SV-LorenaNeural
|
||||||
|
Gender: Female
|
||||||
|
|
||||||
|
Name: es-SV-RodrigoNeural
|
||||||
|
Gender: Male
|
||||||
|
|
||||||
|
Name: es-US-AlonsoNeural
|
||||||
|
Gender: Male
|
||||||
|
|
||||||
|
Name: es-US-PalomaNeural
|
||||||
|
Gender: Female
|
||||||
|
|
||||||
|
Name: es-UY-MateoNeural
|
||||||
|
Gender: Male
|
||||||
|
|
||||||
|
Name: es-UY-ValentinaNeural
|
||||||
|
Gender: Female
|
||||||
|
|
||||||
|
Name: es-VE-PaolaNeural
|
||||||
|
Gender: Female
|
||||||
|
|
||||||
|
Name: es-VE-SebastianNeural
|
||||||
|
Gender: Male
|
||||||
|
|
||||||
|
Name: et-EE-AnuNeural
|
||||||
|
Gender: Female
|
||||||
|
|
||||||
|
Name: et-EE-KertNeural
|
||||||
|
Gender: Male
|
||||||
|
|
||||||
|
Name: fa-IR-DilaraNeural
|
||||||
|
Gender: Female
|
||||||
|
|
||||||
|
Name: fa-IR-FaridNeural
|
||||||
|
Gender: Male
|
||||||
|
|
||||||
|
Name: fi-FI-HarriNeural
|
||||||
|
Gender: Male
|
||||||
|
|
||||||
|
Name: fi-FI-NooraNeural
|
||||||
|
Gender: Female
|
||||||
|
|
||||||
|
Name: fil-PH-AngeloNeural
|
||||||
|
Gender: Male
|
||||||
|
|
||||||
|
Name: fil-PH-BlessicaNeural
|
||||||
|
Gender: Female
|
||||||
|
|
||||||
|
Name: fr-BE-CharlineNeural
|
||||||
|
Gender: Female
|
||||||
|
|
||||||
|
Name: fr-BE-GerardNeural
|
||||||
|
Gender: Male
|
||||||
|
|
||||||
|
Name: fr-CA-AntoineNeural
|
||||||
|
Gender: Male
|
||||||
|
|
||||||
|
Name: fr-CA-JeanNeural
|
||||||
|
Gender: Male
|
||||||
|
|
||||||
|
Name: fr-CA-SylvieNeural
|
||||||
|
Gender: Female
|
||||||
|
|
||||||
|
Name: fr-CA-ThierryNeural
|
||||||
|
Gender: Male
|
||||||
|
|
||||||
|
Name: fr-CH-ArianeNeural
|
||||||
|
Gender: Female
|
||||||
|
|
||||||
|
Name: fr-CH-FabriceNeural
|
||||||
|
Gender: Male
|
||||||
|
|
||||||
|
Name: fr-FR-DeniseNeural
|
||||||
|
Gender: Female
|
||||||
|
|
||||||
|
Name: fr-FR-EloiseNeural
|
||||||
|
Gender: Female
|
||||||
|
|
||||||
|
Name: fr-FR-HenriNeural
|
||||||
|
Gender: Male
|
||||||
|
|
||||||
|
Name: fr-FR-RemyMultilingualNeural
|
||||||
|
Gender: Male
|
||||||
|
|
||||||
|
Name: fr-FR-VivienneMultilingualNeural
|
||||||
|
Gender: Female
|
||||||
|
|
||||||
|
Name: ga-IE-ColmNeural
|
||||||
|
Gender: Male
|
||||||
|
|
||||||
|
Name: ga-IE-OrlaNeural
|
||||||
|
Gender: Female
|
||||||
|
|
||||||
|
Name: gl-ES-RoiNeural
|
||||||
|
Gender: Male
|
||||||
|
|
||||||
|
Name: gl-ES-SabelaNeural
|
||||||
|
Gender: Female
|
||||||
|
|
||||||
|
Name: gu-IN-DhwaniNeural
|
||||||
|
Gender: Female
|
||||||
|
|
||||||
|
Name: gu-IN-NiranjanNeural
|
||||||
|
Gender: Male
|
||||||
|
|
||||||
|
Name: he-IL-AvriNeural
|
||||||
|
Gender: Male
|
||||||
|
|
||||||
|
Name: he-IL-HilaNeural
|
||||||
|
Gender: Female
|
||||||
|
|
||||||
|
Name: hi-IN-MadhurNeural
|
||||||
|
Gender: Male
|
||||||
|
|
||||||
|
Name: hi-IN-SwaraNeural
|
||||||
|
Gender: Female
|
||||||
|
|
||||||
|
Name: hr-HR-GabrijelaNeural
|
||||||
|
Gender: Female
|
||||||
|
|
||||||
|
Name: hr-HR-SreckoNeural
|
||||||
|
Gender: Male
|
||||||
|
|
||||||
|
Name: hu-HU-NoemiNeural
|
||||||
|
Gender: Female
|
||||||
|
|
||||||
|
Name: hu-HU-TamasNeural
|
||||||
|
Gender: Male
|
||||||
|
|
||||||
|
Name: id-ID-ArdiNeural
|
||||||
|
Gender: Male
|
||||||
|
|
||||||
|
Name: id-ID-GadisNeural
|
||||||
|
Gender: Female
|
||||||
|
|
||||||
|
Name: is-IS-GudrunNeural
|
||||||
|
Gender: Female
|
||||||
|
|
||||||
|
Name: is-IS-GunnarNeural
|
||||||
|
Gender: Male
|
||||||
|
|
||||||
|
Name: it-IT-DiegoNeural
|
||||||
|
Gender: Male
|
||||||
|
|
||||||
|
Name: it-IT-ElsaNeural
|
||||||
|
Gender: Female
|
||||||
|
|
||||||
|
Name: it-IT-GiuseppeNeural
|
||||||
|
Gender: Male
|
||||||
|
|
||||||
|
Name: it-IT-IsabellaNeural
|
||||||
|
Gender: Female
|
||||||
|
|
||||||
|
Name: ja-JP-KeitaNeural
|
||||||
|
Gender: Male
|
||||||
|
|
||||||
|
Name: ja-JP-NanamiNeural
|
||||||
|
Gender: Female
|
||||||
|
|
||||||
|
Name: jv-ID-DimasNeural
|
||||||
|
Gender: Male
|
||||||
|
|
||||||
|
Name: jv-ID-SitiNeural
|
||||||
|
Gender: Female
|
||||||
|
|
||||||
|
Name: ka-GE-EkaNeural
|
||||||
|
Gender: Female
|
||||||
|
|
||||||
|
Name: ka-GE-GiorgiNeural
|
||||||
|
Gender: Male
|
||||||
|
|
||||||
|
Name: kk-KZ-AigulNeural
|
||||||
|
Gender: Female
|
||||||
|
|
||||||
|
Name: kk-KZ-DauletNeural
|
||||||
|
Gender: Male
|
||||||
|
|
||||||
|
Name: km-KH-PisethNeural
|
||||||
|
Gender: Male
|
||||||
|
|
||||||
|
Name: km-KH-SreymomNeural
|
||||||
|
Gender: Female
|
||||||
|
|
||||||
|
Name: kn-IN-GaganNeural
|
||||||
|
Gender: Male
|
||||||
|
|
||||||
|
Name: kn-IN-SapnaNeural
|
||||||
|
Gender: Female
|
||||||
|
|
||||||
|
Name: ko-KR-HyunsuNeural
|
||||||
|
Gender: Male
|
||||||
|
|
||||||
|
Name: ko-KR-InJoonNeural
|
||||||
|
Gender: Male
|
||||||
|
|
||||||
|
Name: ko-KR-SunHiNeural
|
||||||
|
Gender: Female
|
||||||
|
|
||||||
|
Name: lo-LA-ChanthavongNeural
|
||||||
|
Gender: Male
|
||||||
|
|
||||||
|
Name: lo-LA-KeomanyNeural
|
||||||
|
Gender: Female
|
||||||
|
|
||||||
|
Name: lt-LT-LeonasNeural
|
||||||
|
Gender: Male
|
||||||
|
|
||||||
|
Name: lt-LT-OnaNeural
|
||||||
|
Gender: Female
|
||||||
|
|
||||||
|
Name: lv-LV-EveritaNeural
|
||||||
|
Gender: Female
|
||||||
|
|
||||||
|
Name: lv-LV-NilsNeural
|
||||||
|
Gender: Male
|
||||||
|
|
||||||
|
Name: mk-MK-AleksandarNeural
|
||||||
|
Gender: Male
|
||||||
|
|
||||||
|
Name: mk-MK-MarijaNeural
|
||||||
|
Gender: Female
|
||||||
|
|
||||||
|
Name: ml-IN-MidhunNeural
|
||||||
|
Gender: Male
|
||||||
|
|
||||||
|
Name: ml-IN-SobhanaNeural
|
||||||
|
Gender: Female
|
||||||
|
|
||||||
|
Name: mn-MN-BataaNeural
|
||||||
|
Gender: Male
|
||||||
|
|
||||||
|
Name: mn-MN-YesuiNeural
|
||||||
|
Gender: Female
|
||||||
|
|
||||||
|
Name: mr-IN-AarohiNeural
|
||||||
|
Gender: Female
|
||||||
|
|
||||||
|
Name: mr-IN-ManoharNeural
|
||||||
|
Gender: Male
|
||||||
|
|
||||||
|
Name: ms-MY-OsmanNeural
|
||||||
|
Gender: Male
|
||||||
|
|
||||||
|
Name: ms-MY-YasminNeural
|
||||||
|
Gender: Female
|
||||||
|
|
||||||
|
Name: mt-MT-GraceNeural
|
||||||
|
Gender: Female
|
||||||
|
|
||||||
|
Name: mt-MT-JosephNeural
|
||||||
|
Gender: Male
|
||||||
|
|
||||||
|
Name: my-MM-NilarNeural
|
||||||
|
Gender: Female
|
||||||
|
|
||||||
|
Name: my-MM-ThihaNeural
|
||||||
|
Gender: Male
|
||||||
|
|
||||||
|
Name: nb-NO-FinnNeural
|
||||||
|
Gender: Male
|
||||||
|
|
||||||
|
Name: nb-NO-PernilleNeural
|
||||||
|
Gender: Female
|
||||||
|
|
||||||
|
Name: ne-NP-HemkalaNeural
|
||||||
|
Gender: Female
|
||||||
|
|
||||||
|
Name: ne-NP-SagarNeural
|
||||||
|
Gender: Male
|
||||||
|
|
||||||
|
Name: nl-BE-ArnaudNeural
|
||||||
|
Gender: Male
|
||||||
|
|
||||||
|
Name: nl-BE-DenaNeural
|
||||||
|
Gender: Female
|
||||||
|
|
||||||
|
Name: nl-NL-ColetteNeural
|
||||||
|
Gender: Female
|
||||||
|
|
||||||
|
Name: nl-NL-FennaNeural
|
||||||
|
Gender: Female
|
||||||
|
|
||||||
|
Name: nl-NL-MaartenNeural
|
||||||
|
Gender: Male
|
||||||
|
|
||||||
|
Name: pl-PL-MarekNeural
|
||||||
|
Gender: Male
|
||||||
|
|
||||||
|
Name: pl-PL-ZofiaNeural
|
||||||
|
Gender: Female
|
||||||
|
|
||||||
|
Name: ps-AF-GulNawazNeural
|
||||||
|
Gender: Male
|
||||||
|
|
||||||
|
Name: ps-AF-LatifaNeural
|
||||||
|
Gender: Female
|
||||||
|
|
||||||
|
Name: pt-BR-AntonioNeural
|
||||||
|
Gender: Male
|
||||||
|
|
||||||
|
Name: pt-BR-FranciscaNeural
|
||||||
|
Gender: Female
|
||||||
|
|
||||||
|
Name: pt-BR-ThalitaNeural
|
||||||
|
Gender: Female
|
||||||
|
|
||||||
|
Name: pt-PT-DuarteNeural
|
||||||
|
Gender: Male
|
||||||
|
|
||||||
|
Name: pt-PT-RaquelNeural
|
||||||
|
Gender: Female
|
||||||
|
|
||||||
|
Name: ro-RO-AlinaNeural
|
||||||
|
Gender: Female
|
||||||
|
|
||||||
|
Name: ro-RO-EmilNeural
|
||||||
|
Gender: Male
|
||||||
|
|
||||||
|
Name: ru-RU-DmitryNeural
|
||||||
|
Gender: Male
|
||||||
|
|
||||||
|
Name: ru-RU-SvetlanaNeural
|
||||||
|
Gender: Female
|
||||||
|
|
||||||
|
Name: si-LK-SameeraNeural
|
||||||
|
Gender: Male
|
||||||
|
|
||||||
|
Name: si-LK-ThiliniNeural
|
||||||
|
Gender: Female
|
||||||
|
|
||||||
|
Name: sk-SK-LukasNeural
|
||||||
|
Gender: Male
|
||||||
|
|
||||||
|
Name: sk-SK-ViktoriaNeural
|
||||||
|
Gender: Female
|
||||||
|
|
||||||
|
Name: sl-SI-PetraNeural
|
||||||
|
Gender: Female
|
||||||
|
|
||||||
|
Name: sl-SI-RokNeural
|
||||||
|
Gender: Male
|
||||||
|
|
||||||
|
Name: so-SO-MuuseNeural
|
||||||
|
Gender: Male
|
||||||
|
|
||||||
|
Name: so-SO-UbaxNeural
|
||||||
|
Gender: Female
|
||||||
|
|
||||||
|
Name: sq-AL-AnilaNeural
|
||||||
|
Gender: Female
|
||||||
|
|
||||||
|
Name: sq-AL-IlirNeural
|
||||||
|
Gender: Male
|
||||||
|
|
||||||
|
Name: sr-RS-NicholasNeural
|
||||||
|
Gender: Male
|
||||||
|
|
||||||
|
Name: sr-RS-SophieNeural
|
||||||
|
Gender: Female
|
||||||
|
|
||||||
|
Name: su-ID-JajangNeural
|
||||||
|
Gender: Male
|
||||||
|
|
||||||
|
Name: su-ID-TutiNeural
|
||||||
|
Gender: Female
|
||||||
|
|
||||||
|
Name: sv-SE-MattiasNeural
|
||||||
|
Gender: Male
|
||||||
|
|
||||||
|
Name: sv-SE-SofieNeural
|
||||||
|
Gender: Female
|
||||||
|
|
||||||
|
Name: sw-KE-RafikiNeural
|
||||||
|
Gender: Male
|
||||||
|
|
||||||
|
Name: sw-KE-ZuriNeural
|
||||||
|
Gender: Female
|
||||||
|
|
||||||
|
Name: sw-TZ-DaudiNeural
|
||||||
|
Gender: Male
|
||||||
|
|
||||||
|
Name: sw-TZ-RehemaNeural
|
||||||
|
Gender: Female
|
||||||
|
|
||||||
|
Name: ta-IN-PallaviNeural
|
||||||
|
Gender: Female
|
||||||
|
|
||||||
|
Name: ta-IN-ValluvarNeural
|
||||||
|
Gender: Male
|
||||||
|
|
||||||
|
Name: ta-LK-KumarNeural
|
||||||
|
Gender: Male
|
||||||
|
|
||||||
|
Name: ta-LK-SaranyaNeural
|
||||||
|
Gender: Female
|
||||||
|
|
||||||
|
Name: ta-MY-KaniNeural
|
||||||
|
Gender: Female
|
||||||
|
|
||||||
|
Name: ta-MY-SuryaNeural
|
||||||
|
Gender: Male
|
||||||
|
|
||||||
|
Name: ta-SG-AnbuNeural
|
||||||
|
Gender: Male
|
||||||
|
|
||||||
|
Name: ta-SG-VenbaNeural
|
||||||
|
Gender: Female
|
||||||
|
|
||||||
|
Name: te-IN-MohanNeural
|
||||||
|
Gender: Male
|
||||||
|
|
||||||
|
Name: te-IN-ShrutiNeural
|
||||||
|
Gender: Female
|
||||||
|
|
||||||
|
Name: th-TH-NiwatNeural
|
||||||
|
Gender: Male
|
||||||
|
|
||||||
|
Name: th-TH-PremwadeeNeural
|
||||||
|
Gender: Female
|
||||||
|
|
||||||
|
Name: tr-TR-AhmetNeural
|
||||||
|
Gender: Male
|
||||||
|
|
||||||
|
Name: tr-TR-EmelNeural
|
||||||
|
Gender: Female
|
||||||
|
|
||||||
|
Name: uk-UA-OstapNeural
|
||||||
|
Gender: Male
|
||||||
|
|
||||||
|
Name: uk-UA-PolinaNeural
|
||||||
|
Gender: Female
|
||||||
|
|
||||||
|
Name: ur-IN-GulNeural
|
||||||
|
Gender: Female
|
||||||
|
|
||||||
|
Name: ur-IN-SalmanNeural
|
||||||
|
Gender: Male
|
||||||
|
|
||||||
|
Name: ur-PK-AsadNeural
|
||||||
|
Gender: Male
|
||||||
|
|
||||||
|
Name: ur-PK-UzmaNeural
|
||||||
|
Gender: Female
|
||||||
|
|
||||||
|
Name: uz-UZ-MadinaNeural
|
||||||
|
Gender: Female
|
||||||
|
|
||||||
|
Name: uz-UZ-SardorNeural
|
||||||
|
Gender: Male
|
||||||
|
|
||||||
|
Name: vi-VN-HoaiMyNeural
|
||||||
|
Gender: Female
|
||||||
|
|
||||||
|
Name: vi-VN-NamMinhNeural
|
||||||
|
Gender: Male
|
||||||
|
|
||||||
|
Name: zh-CN-XiaoxiaoNeural
|
||||||
|
Gender: Female
|
||||||
|
|
||||||
|
Name: zh-CN-XiaoyiNeural
|
||||||
|
Gender: Female
|
||||||
|
|
||||||
|
Name: zh-CN-YunjianNeural
|
||||||
|
Gender: Male
|
||||||
|
|
||||||
|
Name: zh-CN-YunxiNeural
|
||||||
|
Gender: Male
|
||||||
|
|
||||||
|
Name: zh-CN-YunxiaNeural
|
||||||
|
Gender: Male
|
||||||
|
|
||||||
|
Name: zh-CN-YunyangNeural
|
||||||
|
Gender: Male
|
||||||
|
|
||||||
|
Name: zh-CN-liaoning-XiaobeiNeural
|
||||||
|
Gender: Female
|
||||||
|
|
||||||
|
Name: zh-CN-shaanxi-XiaoniNeural
|
||||||
|
Gender: Female
|
||||||
|
|
||||||
|
Name: zh-HK-HiuGaaiNeural
|
||||||
|
Gender: Female
|
||||||
|
|
||||||
|
Name: zh-HK-HiuMaanNeural
|
||||||
|
Gender: Female
|
||||||
|
|
||||||
|
Name: zh-HK-WanLungNeural
|
||||||
|
Gender: Male
|
||||||
|
|
||||||
|
Name: zh-TW-HsiaoChenNeural
|
||||||
|
Gender: Female
|
||||||
|
|
||||||
|
Name: zh-TW-HsiaoYuNeural
|
||||||
|
Gender: Female
|
||||||
|
|
||||||
|
Name: zh-TW-YunJheNeural
|
||||||
|
Gender: Male
|
||||||
|
|
||||||
|
Name: zu-ZA-ThandoNeural
|
||||||
|
Gender: Female
|
||||||
|
|
||||||
|
Name: zu-ZA-ThembaNeural
|
||||||
|
Gender: Male
|
||||||
BIN
sites/docs/.vuepress/public/webui copy.jpg
Normal file
|
After Width: | Height: | Size: 340 KiB |
BIN
sites/docs/.vuepress/public/webui-en.jpg
Normal file
|
After Width: | Height: | Size: 384 KiB |
BIN
sites/docs/.vuepress/public/webui.jpg
Normal file
|
After Width: | Height: | Size: 340 KiB |
BIN
sites/docs/.vuepress/public/wechat-04.jpg
Normal file
|
After Width: | Height: | Size: 166 KiB |
16
sites/docs/README.md
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
---
|
||||||
|
home: true
|
||||||
|
heroImage: /hero.png
|
||||||
|
actions:
|
||||||
|
- text: Get Started →
|
||||||
|
link: /guide/
|
||||||
|
type: primary
|
||||||
|
features:
|
||||||
|
- title: Multilingual
|
||||||
|
details: Supports video scripts in both Chinese and English; offers multiple voice synthesis options.
|
||||||
|
- title: Maintainability
|
||||||
|
details: Complete MVC architecture with clear code structure, easy to maintain, supports both API and Web interface.
|
||||||
|
- title: Multi-Model Support
|
||||||
|
details: Supports integration with multiple models including OpenAI, moonshot, Azure, gpt4free, one-api, Tongyi Qianwen, Google Gemini, Ollama, and others.
|
||||||
|
footer: MIT Licensed | Copyright © 2024-present MoneyPrinterTurbo
|
||||||
|
---
|
||||||
134
sites/docs/guide/README.md
Normal file
@@ -0,0 +1,134 @@
|
|||||||
|
## Installation & Deployment 📥
|
||||||
|
|
||||||
|
Simply provide a <b>topic</b> or <b>keyword</b> for a video, and it will automatically generate the video copy, video
|
||||||
|
materials, video subtitles, and video background music before synthesizing a high-definition short video.
|
||||||
|
|
||||||
|
### WebUI
|
||||||
|
|
||||||
|

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

|
||||||
|
|
||||||
|
- Try to avoid using **Chinese paths** to prevent unpredictable issues
|
||||||
|
- Ensure your **network** is stable, meaning you can access foreign websites normally
|
||||||
|
|
||||||
|
#### ① Clone the Project
|
||||||
|
|
||||||
|
```shell
|
||||||
|
git clone https://github.com/harry0703/MoneyPrinterTurbo.git
|
||||||
|
```
|
||||||
|
|
||||||
|
#### ② Modify the Configuration File
|
||||||
|
|
||||||
|
- Copy the `config.example.toml` file and rename it to `config.toml`
|
||||||
|
- Follow the instructions in the `config.toml` file to configure `pexels_api_keys` and `llm_provider`, and according to
|
||||||
|
the llm_provider's service provider, set up the corresponding API Key
|
||||||
|
|
||||||
|
#### ③ Configure Large Language Models (LLM)
|
||||||
|
|
||||||
|
- To use `GPT-4.0` or `GPT-3.5`, you need an `API Key` from `OpenAI`. If you don't have one, you can set `llm_provider`
|
||||||
|
to `g4f` (a free-to-use GPT library https://github.com/xtekky/gpt4free)
|
||||||
|
|
||||||
|
### Docker Deployment 🐳
|
||||||
|
|
||||||
|
#### ① Launch the Docker Container
|
||||||
|
|
||||||
|
If you haven't installed Docker, please install it first https://www.docker.com/products/docker-desktop/
|
||||||
|
If you are using a Windows system, please refer to Microsoft's documentation:
|
||||||
|
|
||||||
|
1. https://learn.microsoft.com/en-us/windows/wsl/install
|
||||||
|
2. https://learn.microsoft.com/en-us/windows/wsl/tutorials/wsl-containers
|
||||||
|
|
||||||
|
```shell
|
||||||
|
cd MoneyPrinterTurbo
|
||||||
|
docker-compose up
|
||||||
|
```
|
||||||
|
|
||||||
|
#### ② Access the Web Interface
|
||||||
|
|
||||||
|
Open your browser and visit http://0.0.0.0:8501
|
||||||
|
|
||||||
|
#### ③ Access the API Interface
|
||||||
|
|
||||||
|
Open your browser and visit http://0.0.0.0:8080/docs Or http://0.0.0.0:8080/redoc
|
||||||
|
|
||||||
|
### Manual Deployment 📦
|
||||||
|
|
||||||
|
#### ① Create a Python Virtual Environment
|
||||||
|
|
||||||
|
It is recommended to create a Python virtual environment
|
||||||
|
using [conda](https://conda.io/projects/conda/en/latest/user-guide/install/index.html)
|
||||||
|
|
||||||
|
```shell
|
||||||
|
git clone https://github.com/harry0703/MoneyPrinterTurbo.git
|
||||||
|
cd MoneyPrinterTurbo
|
||||||
|
conda create -n MoneyPrinterTurbo python=3.10
|
||||||
|
conda activate MoneyPrinterTurbo
|
||||||
|
pip install -r requirements.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
#### ② Install ImageMagick
|
||||||
|
|
||||||
|
###### Windows:
|
||||||
|
|
||||||
|
- Download https://imagemagick.org/archive/binaries/ImageMagick-7.1.1-29-Q16-x64-static.exe
|
||||||
|
- Install the downloaded ImageMagick, **do not change the installation path**
|
||||||
|
- Modify the `config.toml` configuration file, set `imagemagick_path` to your actual installation path (if you didn't
|
||||||
|
change the path during installation, just uncomment it)
|
||||||
|
|
||||||
|
###### MacOS:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
brew install imagemagick
|
||||||
|
```
|
||||||
|
|
||||||
|
###### Ubuntu
|
||||||
|
|
||||||
|
```shell
|
||||||
|
sudo apt-get install imagemagick
|
||||||
|
```
|
||||||
|
|
||||||
|
###### CentOS
|
||||||
|
|
||||||
|
```shell
|
||||||
|
sudo yum install ImageMagick
|
||||||
|
```
|
||||||
|
|
||||||
|
#### ③ Launch the Web Interface 🌐
|
||||||
|
|
||||||
|
Note that you need to execute the following commands in the `root directory` of the MoneyPrinterTurbo project
|
||||||
|
|
||||||
|
###### Windows
|
||||||
|
|
||||||
|
```bat
|
||||||
|
conda activate MoneyPrinterTurbo
|
||||||
|
webui.bat
|
||||||
|
```
|
||||||
|
|
||||||
|
###### MacOS or Linux
|
||||||
|
|
||||||
|
```shell
|
||||||
|
conda activate MoneyPrinterTurbo
|
||||||
|
sh webui.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
After launching, the browser will open automatically
|
||||||
|
|
||||||
|
#### ④ Launch the API Service 🚀
|
||||||
|
|
||||||
|
```shell
|
||||||
|
python main.py
|
||||||
|
```
|
||||||
|
|
||||||
|
After launching, you can view the `API documentation` at http://127.0.0.1:8080/docs and directly test the interface
|
||||||
|
online for a quick experience.
|
||||||
|
|
||||||
|
## License 📝
|
||||||
|
|
||||||
|
Click to view the [`LICENSE`](LICENSE) file
|
||||||
|
|
||||||
|
## Star History
|
||||||
|
|
||||||
|
[](https://star-history.com/#harry0703/MoneyPrinterTurbo&Date)
|
||||||
5
sites/docs/guide/background-music.md
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
## Background Music 🎵
|
||||||
|
|
||||||
|
Background music for videos is located in the project's `resource/songs` directory.
|
||||||
|
> The current project includes some default music from YouTube videos. If there are copyright issues, please delete
|
||||||
|
> them.
|
||||||
70
sites/docs/guide/faq.md
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
## Common Questions 🤔
|
||||||
|
|
||||||
|
### ❓How to Use the Free OpenAI GPT-3.5 Model?
|
||||||
|
|
||||||
|
[OpenAI has announced that ChatGPT with 3.5 is now free](https://openai.com/blog/start-using-chatgpt-instantly), and
|
||||||
|
developers have wrapped it into an API for direct usage.
|
||||||
|
|
||||||
|
**Ensure you have Docker installed and running**. Execute the following command to start the Docker service:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
docker run -p 3040:3040 missuo/freegpt35
|
||||||
|
```
|
||||||
|
|
||||||
|
Once successfully started, modify the `config.toml` configuration as follows:
|
||||||
|
|
||||||
|
- Set `llm_provider` to `openai`
|
||||||
|
- Fill in `openai_api_key` with any value, for example, '123456'
|
||||||
|
- Change `openai_base_url` to `http://localhost:3040/v1/`
|
||||||
|
- Set `openai_model_name` to `gpt-3.5-turbo`
|
||||||
|
|
||||||
|
### ❓RuntimeError: No ffmpeg exe could be found
|
||||||
|
|
||||||
|
Normally, ffmpeg will be automatically downloaded and detected.
|
||||||
|
However, if your environment has issues preventing automatic downloads, you may encounter the following error:
|
||||||
|
|
||||||
|
```
|
||||||
|
RuntimeError: No ffmpeg exe could be found.
|
||||||
|
Install ffmpeg on your system, or set the IMAGEIO_FFMPEG_EXE environment variable.
|
||||||
|
```
|
||||||
|
|
||||||
|
In this case, you can download ffmpeg from https://www.gyan.dev/ffmpeg/builds/, unzip it, and set `ffmpeg_path` to your
|
||||||
|
actual installation path.
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[app]
|
||||||
|
# Please set according to your actual path, note that Windows path separators are \\
|
||||||
|
ffmpeg_path = "C:\\Users\\harry\\Downloads\\ffmpeg.exe"
|
||||||
|
```
|
||||||
|
|
||||||
|
### ❓Error generating audio or downloading videos
|
||||||
|
|
||||||
|
[issue 56](https://github.com/harry0703/MoneyPrinterTurbo/issues/56)
|
||||||
|
|
||||||
|
```
|
||||||
|
failed to generate audio, maybe the network is not available.
|
||||||
|
if you are in China, please use a VPN.
|
||||||
|
```
|
||||||
|
|
||||||
|
[issue 44](https://github.com/harry0703/MoneyPrinterTurbo/issues/44)
|
||||||
|
|
||||||
|
```
|
||||||
|
failed to download videos, maybe the network is not available.
|
||||||
|
if you are in China, please use a VPN.
|
||||||
|
```
|
||||||
|
|
||||||
|
This is likely due to network issues preventing access to foreign services. Please use a VPN to resolve this.
|
||||||
|
|
||||||
|
### ❓ImageMagick is not installed on your computer
|
||||||
|
|
||||||
|
[issue 33](https://github.com/harry0703/MoneyPrinterTurbo/issues/33)
|
||||||
|
|
||||||
|
1. Follow the `example configuration` provided `download address` to
|
||||||
|
install https://imagemagick.org/archive/binaries/ImageMagick-7.1.1-30-Q16-x64-static.exe, using the static library
|
||||||
|
2. Do not install in a path with Chinese characters to avoid unpredictable issues
|
||||||
|
|
||||||
|
[issue 54](https://github.com/harry0703/MoneyPrinterTurbo/issues/54#issuecomment-2017842022)
|
||||||
|
|
||||||
|
For Linux systems, you can manually install it, refer to https://cn.linux-console.net/?p=16978
|
||||||
|
|
||||||
|
Thanks to [@wangwenqiao666](https://github.com/wangwenqiao666) for their research and exploration
|
||||||
34
sites/docs/guide/features.md
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
## Features 🎯
|
||||||
|
|
||||||
|
- [x] Complete **MVC architecture**, **clearly structured** code, easy to maintain, supports both `API`
|
||||||
|
and `Web interface`
|
||||||
|
- [x] Supports **AI-generated** video copy, as well as **customized copy**
|
||||||
|
- [x] Supports various **high-definition video** sizes
|
||||||
|
- [x] Portrait 9:16, `1080x1920`
|
||||||
|
- [x] Landscape 16:9, `1920x1080`
|
||||||
|
- [x] Supports **batch video generation**, allowing the creation of multiple videos at once, then selecting the most
|
||||||
|
satisfactory one
|
||||||
|
- [x] Supports setting the **duration of video clips**, facilitating adjustments to material switching frequency
|
||||||
|
- [x] Supports video copy in both **Chinese** and **English**
|
||||||
|
- [x] Supports **multiple voice** synthesis
|
||||||
|
- [x] Supports **subtitle generation**, with adjustable `font`, `position`, `color`, `size`, and also
|
||||||
|
supports `subtitle outlining`
|
||||||
|
- [x] Supports **background music**, either random or specified music files, with adjustable `background music volume`
|
||||||
|
- [x] Video material sources are **high-definition** and **royalty-free**
|
||||||
|
- [x] Supports integration with various models such as **OpenAI**, **moonshot**, **Azure**, **gpt4free**, **one-api**,
|
||||||
|
**qianwen**, **Google Gemini**, **Ollama** and more
|
||||||
|
|
||||||
|
❓[How to Use the Free OpenAI GPT-3.5 Model?](https://github.com/harry0703/MoneyPrinterTurbo/blob/main/README-en.md#common-questions-)
|
||||||
|
|
||||||
|
### Future Plans 📅
|
||||||
|
|
||||||
|
- [ ] Introduce support for GPT-SoVITS dubbing
|
||||||
|
- [ ] Enhance voice synthesis with large models for a more natural and emotionally resonant voice output
|
||||||
|
- [ ] Incorporate video transition effects to ensure a smoother viewing experience
|
||||||
|
- [ ] Improve the relevance of video content
|
||||||
|
- [ ] Add options for video length: short, medium, long
|
||||||
|
- [ ] Package the application into a one-click launch bundle for Windows and macOS for ease of use
|
||||||
|
- [ ] Enable the use of custom materials
|
||||||
|
- [ ] Offer voiceover and background music options with real-time preview
|
||||||
|
- [ ] Support a wider range of voice synthesis providers, such as OpenAI TTS, Azure TTS
|
||||||
|
- [ ] Automate the upload process to the YouTube platform
|
||||||
4
sites/docs/guide/feedback.md
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
## Feedback & Suggestions 📢
|
||||||
|
|
||||||
|
- You can submit an [issue](https://github.com/harry0703/MoneyPrinterTurbo/issues) or
|
||||||
|
a [pull request](https://github.com/harry0703/MoneyPrinterTurbo/pulls).
|
||||||
4
sites/docs/guide/reference-project.md
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
## Reference Projects 📚
|
||||||
|
|
||||||
|
This project is based on https://github.com/FujiwaraChoki/MoneyPrinter and has been refactored with a lot of
|
||||||
|
optimizations and added functionalities. Thanks to the original author for their spirit of open source.
|
||||||
3
sites/docs/guide/speech-synthesis.md
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
## Voice Synthesis 🗣
|
||||||
|
|
||||||
|
A list of all supported voices can be viewed here: [Voice List](/voice-list.txt)
|
||||||
4
sites/docs/guide/subtitle-font.md
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
## Subtitle Fonts 🅰
|
||||||
|
|
||||||
|
Fonts for rendering video subtitles are located in the project's `resource/fonts` directory, and you can also add your
|
||||||
|
own fonts.
|
||||||
15
sites/docs/guide/subtitle-generation.md
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
## Subtitle Generation 📜
|
||||||
|
|
||||||
|
Currently, there are 2 ways to generate subtitles:
|
||||||
|
|
||||||
|
- edge: Faster generation speed, better performance, no specific requirements for computer configuration, but the
|
||||||
|
quality may be unstable
|
||||||
|
- whisper: Slower generation speed, poorer performance, specific requirements for computer configuration, but more
|
||||||
|
reliable quality
|
||||||
|
|
||||||
|
You can switch between them by modifying the `subtitle_provider` in the `config.toml` configuration file
|
||||||
|
|
||||||
|
It is recommended to use `edge` mode, and switch to `whisper` mode if the quality of the subtitles generated is not
|
||||||
|
satisfactory.
|
||||||
|
|
||||||
|
> If left blank, it means no subtitles will be generated.
|
||||||
35
sites/docs/guide/video-demonstration.md
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
## Video Demos 📺
|
||||||
|
|
||||||
|
### Portrait 9:16
|
||||||
|
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th align="center"><g-emoji class="g-emoji" alias="arrow_forward">▶️</g-emoji> How to Add Fun to Your Life </th>
|
||||||
|
<th align="center"><g-emoji class="g-emoji" alias="arrow_forward">▶️</g-emoji> What is the Meaning of Life</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td align="center"><video src="https://github.com/harry0703/MoneyPrinterTurbo/assets/4928832/a84d33d5-27a2-4aba-8fd0-9fb2bd91c6a6"></video></td>
|
||||||
|
<td align="center"><video src="https://github.com/harry0703/MoneyPrinterTurbo/assets/4928832/112c9564-d52b-4472-99ad-970b75f66476"></video></td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
### Landscape 16:9
|
||||||
|
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th align="center"><g-emoji class="g-emoji" alias="arrow_forward">▶️</g-emoji> What is the Meaning of Life</th>
|
||||||
|
<th align="center"><g-emoji class="g-emoji" alias="arrow_forward">▶️</g-emoji> Why Exercise</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td align="center"><video src="https://github.com/harry0703/MoneyPrinterTurbo/assets/4928832/346ebb15-c55f-47a9-a653-114f08bb8073"></video></td>
|
||||||
|
<td align="center"><video src="https://github.com/harry0703/MoneyPrinterTurbo/assets/4928832/271f2fae-8283-44a0-8aa0-0ed8f9a6fa87"></video></td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
16
sites/docs/zh/README.md
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
---
|
||||||
|
home: true
|
||||||
|
heroImage: /hero.png
|
||||||
|
actions:
|
||||||
|
- text: 快速上手 →
|
||||||
|
link: /zh/guide/
|
||||||
|
type: primary
|
||||||
|
features:
|
||||||
|
- title: 多语言
|
||||||
|
details: 支持 中文 和 英文 视频文案;支持 多种语音 合成。
|
||||||
|
- title: 可维护性
|
||||||
|
details: 完整的 MVC架构,代码 结构清晰,易于维护,支持 API 和 Web界面。
|
||||||
|
- title: 多模型支持
|
||||||
|
details: 支持 OpenAI、moonshot、Azure、gpt4free、one-api、通义千问、Google Gemini、Ollama 等多种模型接入。
|
||||||
|
footer: MIT Licensed | Copyright © 2024-present MoneyPrinterTurbo
|
||||||
|
---
|
||||||
157
sites/docs/zh/guide/README.md
Normal file
@@ -0,0 +1,157 @@
|
|||||||
|
## 快速开始 🚀
|
||||||
|
|
||||||
|
<br>
|
||||||
|
只需提供一个视频 <b>主题</b> 或 <b>关键词</b> ,就可以全自动生成视频文案、视频素材、视频字幕、视频背景音乐,然后合成一个高清的短视频。
|
||||||
|
<br>
|
||||||
|
|
||||||
|
<h4>Web界面</h4>
|
||||||
|
|
||||||
|

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

|
||||||
|
|
||||||
|
下载一键启动包,解压直接使用
|
||||||
|
|
||||||
|
### Windows
|
||||||
|
|
||||||
|
- 百度网盘: https://pan.baidu.com/s/1bpGjgQVE5sADZRn3A6F87w?pwd=xt16 提取码: xt16
|
||||||
|
|
||||||
|
下载后,建议先**双击执行** `update.bat` 更新到**最新代码**,然后双击 `start.bat` 启动 Web 界面
|
||||||
|
|
||||||
|
### 其他系统
|
||||||
|
|
||||||
|
还没有制作一键启动包,看下面的 **安装部署** 部分,建议使用 **docker** 部署,更加方便。
|
||||||
|
|
||||||
|
## 安装部署 📥
|
||||||
|
|
||||||
|
### 前提条件
|
||||||
|
|
||||||
|
- 尽量不要使用 **中文路径**,避免出现一些无法预料的问题
|
||||||
|
- 请确保你的 **网络** 是正常的,VPN 需要打开`全局流量`模式
|
||||||
|
|
||||||
|
#### ① 克隆代码
|
||||||
|
|
||||||
|
```shell
|
||||||
|
git clone https://github.com/harry0703/MoneyPrinterTurbo.git
|
||||||
|
```
|
||||||
|
|
||||||
|
#### ② 修改配置文件
|
||||||
|
|
||||||
|
- 将 `config.example.toml` 文件复制一份,命名为 `config.toml`
|
||||||
|
- 按照 `config.toml` 文件中的说明,配置好 `pexels_api_keys` 和 `llm_provider`,并根据 llm_provider 对应的服务商,配置相关的
|
||||||
|
API Key
|
||||||
|
|
||||||
|
#### ③ 配置大模型(LLM)
|
||||||
|
|
||||||
|
- 如果要使用 `GPT-4.0` 或 `GPT-3.5`,需要有 `OpenAI` 的 `API Key`,如果没有,可以将 `llm_provider` 设置为 `g4f` (
|
||||||
|
一个免费使用 GPT 的开源库 https://github.com/xtekky/gpt4free ,但是该免费的服务,稳定性较差,有时候可以用,有时候用不了)
|
||||||
|
- 或者可以使用到 [月之暗面](https://platform.moonshot.cn/console/api-keys) 申请。注册就送
|
||||||
|
15 元体验金,可以对话 1500 次左右。然后设置 `llm_provider="moonshot"` 和 `moonshot_api_key`
|
||||||
|
- 也可以使用 通义千问,具体请看配置文件里面的注释说明
|
||||||
|
|
||||||
|
### Docker 部署 🐳
|
||||||
|
|
||||||
|
#### ① 启动 Docker
|
||||||
|
|
||||||
|
如果未安装 Docker,请先安装 https://www.docker.com/products/docker-desktop/
|
||||||
|
|
||||||
|
如果是 Windows 系统,请参考微软的文档:
|
||||||
|
|
||||||
|
1. https://learn.microsoft.com/zh-cn/windows/wsl/install
|
||||||
|
2. https://learn.microsoft.com/zh-cn/windows/wsl/tutorials/wsl-containers
|
||||||
|
|
||||||
|
```shell
|
||||||
|
cd MoneyPrinterTurbo
|
||||||
|
docker-compose up
|
||||||
|
```
|
||||||
|
|
||||||
|
#### ② 访问 Web 界面
|
||||||
|
|
||||||
|
打开浏览器,访问 http://0.0.0.0:8501
|
||||||
|
|
||||||
|
#### ③ 访问 API 文档
|
||||||
|
|
||||||
|
打开浏览器,访问 http://0.0.0.0:8080/docs 或者 http://0.0.0.0:8080/redoc
|
||||||
|
|
||||||
|
### 手动部署 📦
|
||||||
|
|
||||||
|
> 视频教程
|
||||||
|
|
||||||
|
- 完整的使用演示:https://v.douyin.com/iFhnwsKY/
|
||||||
|
- 如何在 Windows 上部署:https://v.douyin.com/iFyjoW3M
|
||||||
|
|
||||||
|
#### ① 创建虚拟环境
|
||||||
|
|
||||||
|
建议使用 [conda](https://conda.io/projects/conda/en/latest/user-guide/install/index.html) 创建 python 虚拟环境
|
||||||
|
|
||||||
|
```shell
|
||||||
|
git clone https://github.com/harry0703/MoneyPrinterTurbo.git
|
||||||
|
cd MoneyPrinterTurbo
|
||||||
|
conda create -n MoneyPrinterTurbo python=3.10
|
||||||
|
conda activate MoneyPrinterTurbo
|
||||||
|
pip install -r requirements.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
#### ② 安装好 ImageMagick
|
||||||
|
|
||||||
|
###### Windows:
|
||||||
|
|
||||||
|
- 下载 https://imagemagick.org/archive/binaries/ImageMagick-7.1.1-30-Q16-x64-static.exe
|
||||||
|
- 安装下载好的 ImageMagick,注意不要修改安装路径
|
||||||
|
- 修改 `配置文件 config.toml` 中的 `imagemagick_path` 为你的实际安装路径(如果安装的时候没有修改路径,直接取消注释即可)
|
||||||
|
|
||||||
|
###### MacOS:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
brew install imagemagick
|
||||||
|
```
|
||||||
|
|
||||||
|
###### Ubuntu
|
||||||
|
|
||||||
|
```shell
|
||||||
|
sudo apt-get install imagemagick
|
||||||
|
```
|
||||||
|
|
||||||
|
###### CentOS
|
||||||
|
|
||||||
|
```shell
|
||||||
|
sudo yum install ImageMagick
|
||||||
|
```
|
||||||
|
|
||||||
|
#### ③ 启动 Web 界面 🌐
|
||||||
|
|
||||||
|
注意需要到 MoneyPrinterTurbo 项目 `根目录` 下执行以下命令
|
||||||
|
|
||||||
|
###### Windows
|
||||||
|
|
||||||
|
```bat
|
||||||
|
conda activate MoneyPrinterTurbo
|
||||||
|
webui.bat
|
||||||
|
```
|
||||||
|
|
||||||
|
###### MacOS or Linux
|
||||||
|
|
||||||
|
```shell
|
||||||
|
conda activate MoneyPrinterTurbo
|
||||||
|
sh webui.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
启动后,会自动打开浏览器
|
||||||
|
|
||||||
|
#### ④ 启动 API 服务 🚀
|
||||||
|
|
||||||
|
```shell
|
||||||
|
python main.py
|
||||||
|
```
|
||||||
|
|
||||||
|
启动后,可以查看 `API文档` http://127.0.0.1:8080/docs 或者 http://127.0.0.1:8080/redoc 直接在线调试接口,快速体验。
|
||||||
|
|
||||||
|
## 许可证 📝
|
||||||
|
|
||||||
|
点击查看 [`LICENSE`](LICENSE) 文件
|
||||||
|
|
||||||
|
## Star History
|
||||||
|
|
||||||
|
[](https://star-history.com/#harry0703/MoneyPrinterTurbo&Date)
|
||||||
4
sites/docs/zh/guide/background-music.md
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
## 背景音乐 🎵
|
||||||
|
|
||||||
|
用于视频的背景音乐,位于项目的 `resource/songs` 目录下。
|
||||||
|
> 当前项目里面放了一些默认的音乐,来自于 YouTube 视频,如有侵权,请删除。
|
||||||
4
sites/docs/zh/guide/configuration-requirements.md
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
## 配置要求 📦
|
||||||
|
|
||||||
|
- 建议最低 CPU 4核或以上,内存 8G 或以上,显卡非必须
|
||||||
|
- Windows 10 或 MacOS 11.0 以上系统
|
||||||
123
sites/docs/zh/guide/faq.md
Normal file
@@ -0,0 +1,123 @@
|
|||||||
|
## 常见问题 🤔
|
||||||
|
|
||||||
|
### ❓如何使用免费的OpenAI GPT-3.5模型?
|
||||||
|
|
||||||
|
[OpenAI宣布ChatGPT里面3.5已经免费了](https://openai.com/blog/start-using-chatgpt-instantly),有开发者将其封装成了API,可以直接调用
|
||||||
|
|
||||||
|
**确保你安装和启动了docker服务**,执行以下命令启动docker服务
|
||||||
|
|
||||||
|
```shell
|
||||||
|
docker run -p 3040:3040 missuo/freegpt35
|
||||||
|
```
|
||||||
|
|
||||||
|
启动成功后,修改 `config.toml` 中的配置
|
||||||
|
|
||||||
|
- `llm_provider` 设置为 `openai`
|
||||||
|
- `openai_api_key` 随便填写一个即可,比如 '123456'
|
||||||
|
- `openai_base_url` 改为 `http://localhost:3040/v1/`
|
||||||
|
- `openai_model_name` 改为 `gpt-3.5-turbo`
|
||||||
|
|
||||||
|
### ❓AttributeError: 'str' object has no attribute 'choices'`
|
||||||
|
|
||||||
|
这个问题是由于 OpenAI 或者其他 LLM,没有返回正确的回复导致的。
|
||||||
|
|
||||||
|
大概率是网络原因, 使用 **VPN**,或者设置 `openai_base_url` 为你的代理 ,应该就可以解决了。
|
||||||
|
|
||||||
|
### ❓RuntimeError: No ffmpeg exe could be found
|
||||||
|
|
||||||
|
通常情况下,ffmpeg 会被自动下载,并且会被自动检测到。
|
||||||
|
但是如果你的环境有问题,无法自动下载,可能会遇到如下错误:
|
||||||
|
|
||||||
|
```
|
||||||
|
RuntimeError: No ffmpeg exe could be found.
|
||||||
|
Install ffmpeg on your system, or set the IMAGEIO_FFMPEG_EXE environment variable.
|
||||||
|
```
|
||||||
|
|
||||||
|
此时你可以从 https://www.gyan.dev/ffmpeg/builds/ 下载ffmpeg,解压后,设置 `ffmpeg_path` 为你的实际安装路径即可。
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[app]
|
||||||
|
# 请根据你的实际路径设置,注意 Windows 路径分隔符为 \\
|
||||||
|
ffmpeg_path = "C:\\Users\\harry\\Downloads\\ffmpeg.exe"
|
||||||
|
```
|
||||||
|
|
||||||
|
### ❓生成音频时报错或下载视频报错
|
||||||
|
|
||||||
|
[issue 56](https://github.com/harry0703/MoneyPrinterTurbo/issues/56)
|
||||||
|
|
||||||
|
```
|
||||||
|
failed to generate audio, maybe the network is not available.
|
||||||
|
if you are in China, please use a VPN.
|
||||||
|
```
|
||||||
|
|
||||||
|
[issue 44](https://github.com/harry0703/MoneyPrinterTurbo/issues/44)
|
||||||
|
|
||||||
|
```
|
||||||
|
failed to download videos, maybe the network is not available.
|
||||||
|
if you are in China, please use a VPN.
|
||||||
|
```
|
||||||
|
|
||||||
|
这个大概率是网络原因,无法访问境外的服务,请使用VPN解决。
|
||||||
|
|
||||||
|
### ❓ImageMagick is not installed on your computer
|
||||||
|
|
||||||
|
[issue 33](https://github.com/harry0703/MoneyPrinterTurbo/issues/33)
|
||||||
|
|
||||||
|
1. 按照 `示例配置` 里面提供的 `下载地址`
|
||||||
|
,安装 https://imagemagick.org/archive/binaries/ImageMagick-7.1.1-29-Q16-x64-static.exe, 用静态库
|
||||||
|
2. 不要安装在中文路径里面,避免出现一些无法预料的问题
|
||||||
|
|
||||||
|
[issue 54](https://github.com/harry0703/MoneyPrinterTurbo/issues/54#issuecomment-2017842022)
|
||||||
|
|
||||||
|
如果是linux系统,可以手动安装,参考 https://cn.linux-console.net/?p=16978
|
||||||
|
|
||||||
|
感谢 [@wangwenqiao666](https://github.com/wangwenqiao666)的研究探索
|
||||||
|
|
||||||
|
### ❓ImageMagick的安全策略阻止了与临时文件@/tmp/tmpur5hyyto.txt相关的操作
|
||||||
|
|
||||||
|
[issue 92](https://github.com/harry0703/MoneyPrinterTurbo/issues/92)
|
||||||
|
|
||||||
|
可以在ImageMagick的配置文件policy.xml中找到这些策略。
|
||||||
|
这个文件通常位于 /etc/ImageMagick-`X`/ 或 ImageMagick 安装目录的类似位置。
|
||||||
|
修改包含`pattern="@"`的条目,将`rights="none"`更改为`rights="read|write"`以允许对文件的读写操作。
|
||||||
|
|
||||||
|
感谢 [@chenhengzh](https://github.com/chenhengzh)的研究探索
|
||||||
|
|
||||||
|
### ❓OSError: [Errno 24] Too many open files
|
||||||
|
|
||||||
|
[issue 100](https://github.com/harry0703/MoneyPrinterTurbo/issues/100)
|
||||||
|
|
||||||
|
这个问题是由于系统打开文件数限制导致的,可以通过修改系统的文件打开数限制来解决。
|
||||||
|
|
||||||
|
查看当前限制
|
||||||
|
|
||||||
|
```shell
|
||||||
|
ulimit -n
|
||||||
|
```
|
||||||
|
|
||||||
|
如果过低,可以调高一些,比如
|
||||||
|
|
||||||
|
```shell
|
||||||
|
ulimit -n 10240
|
||||||
|
```
|
||||||
|
|
||||||
|
### ❓AttributeError: module 'PIL.Image' has no attribute 'ANTIALIAS'
|
||||||
|
|
||||||
|
[issue 101](https://github.com/harry0703/MoneyPrinterTurbo/issues/101),
|
||||||
|
[issue 83](https://github.com/harry0703/MoneyPrinterTurbo/issues/83),
|
||||||
|
[issue 70](https://github.com/harry0703/MoneyPrinterTurbo/issues/70)
|
||||||
|
|
||||||
|
先看下当前的 Pillow 版本是多少
|
||||||
|
|
||||||
|
```shell
|
||||||
|
pip list |grep Pillow
|
||||||
|
```
|
||||||
|
|
||||||
|
如果是 10.x 的版本,可以尝试下降级看看,有用户反馈降级后正常
|
||||||
|
|
||||||
|
```shell
|
||||||
|
pip uninstall Pillow
|
||||||
|
pip install Pillow==9.5.0
|
||||||
|
# 或者降级到 8.4.0
|
||||||
|
pip install Pillow==8.4.0
|
||||||
|
```
|
||||||
31
sites/docs/zh/guide/features.md
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
## 功能特性 🎯
|
||||||
|
|
||||||
|
- [x] 完整的 **MVC架构**,代码 **结构清晰**,易于维护,支持 `API` 和 `Web界面`
|
||||||
|
- [x] 支持视频文案 **AI自动生成**,也可以**自定义文案**
|
||||||
|
- [x] 支持多种 **高清视频** 尺寸
|
||||||
|
- [x] 竖屏 9:16,`1080x1920`
|
||||||
|
- [x] 横屏 16:9,`1920x1080`
|
||||||
|
- [x] 支持 **批量视频生成**,可以一次生成多个视频,然后选择一个最满意的
|
||||||
|
- [x] 支持 **视频片段时长**设置,方便调节素材切换频率
|
||||||
|
- [x] 支持 **中文** 和 **英文** 视频文案
|
||||||
|
- [x] 支持 **多种语音** 合成
|
||||||
|
- [x] 支持 **字幕生成**,可以调整 `字体`、`位置`、`颜色`、`大小`,同时支持`字幕描边`设置
|
||||||
|
- [x] 支持 **背景音乐**,随机或者指定音乐文件,可设置`背景音乐音量`
|
||||||
|
- [x] 视频素材来源 **高清**,而且 **无版权**
|
||||||
|
- [x] 支持 **OpenAI**、**moonshot**、**Azure**、**gpt4free**、**one-api**、**通义千问**、**Google Gemini**、**Ollama** 等多种模型接入
|
||||||
|
|
||||||
|
❓[如何使用免费的 **OpenAI GPT-3.5
|
||||||
|
** 模型?](https://github.com/harry0703/MoneyPrinterTurbo?tab=readme-ov-file#%E5%B8%B8%E8%A7%81%E9%97%AE%E9%A2%98-)
|
||||||
|
|
||||||
|
### 后期计划 📅
|
||||||
|
|
||||||
|
- [ ] GPT-SoVITS 配音支持
|
||||||
|
- [ ] 优化语音合成,利用大模型,使其合成的声音,更加自然,情绪更加丰富
|
||||||
|
- [ ] 增加视频转场效果,使其看起来更加的流畅
|
||||||
|
- [ ] 增加更多视频素材来源,优化视频素材和文案的匹配度
|
||||||
|
- [ ] 增加视频长度选项:短、中、长
|
||||||
|
- [ ] 增加免费网络代理,让访问OpenAI和素材下载不再受限
|
||||||
|
- [ ] 可以使用自己的素材
|
||||||
|
- [ ] 朗读声音和背景音乐,提供实时试听
|
||||||
|
- [ ] 支持更多的语音合成服务商,比如 OpenAI TTS
|
||||||
|
- [ ] 自动上传到YouTube平台
|
||||||
4
sites/docs/zh/guide/feedback.md
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
## 反馈建议 📢
|
||||||
|
|
||||||
|
- 可以提交 [issue](https://github.com/harry0703/MoneyPrinterTurbo/issues)
|
||||||
|
或者 [pull request](https://github.com/harry0703/MoneyPrinterTurbo/pulls)。
|
||||||
4
sites/docs/zh/guide/reference-project.md
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
## 参考项目 📚
|
||||||
|
|
||||||
|
该项目基于 https://github.com/FujiwaraChoki/MoneyPrinter 重构而来,做了大量的优化,增加了更多的功能。
|
||||||
|
感谢原作者的开源精神。
|
||||||
9
sites/docs/zh/guide/special-thanks.md
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
## 特别感谢 🙏
|
||||||
|
|
||||||
|
由于该项目的 **部署** 和 **使用**,对于一些小白用户来说,还是 **有一定的门槛**,在此特别感谢
|
||||||
|
**录咖(AI智能 多媒体服务平台)** 网站基于该项目,提供的免费`AI视频生成器`服务,可以不用部署,直接在线使用,非常方便。
|
||||||
|
|
||||||
|
- 中文版:https://reccloud.cn
|
||||||
|
- 英文版:https://reccloud.com
|
||||||
|
|
||||||
|

|
||||||
5
sites/docs/zh/guide/speech-synthesis.md
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
## 语音合成 🗣
|
||||||
|
|
||||||
|
所有支持的声音列表,可以查看:[声音列表](/voice-list.txt)
|
||||||
|
|
||||||
|
2024-04-16 v1.1.2 新增了9种Azure的语音合成声音,需要配置API KEY,该声音合成的更加真实。
|
||||||
3
sites/docs/zh/guide/subtitle-font.md
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
## 字幕字体 🅰
|
||||||
|
|
||||||
|
用于视频字幕的渲染,位于项目的 `resource/fonts` 目录下,你也可以放进去自己的字体。
|
||||||
36
sites/docs/zh/guide/subtitle-generation.md
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
## 字幕生成 📜
|
||||||
|
|
||||||
|
当前支持2种字幕生成方式:
|
||||||
|
|
||||||
|
- **edge**: 生成`速度快`,性能更好,对电脑配置没有要求,但是质量可能不稳定
|
||||||
|
- **whisper**: 生成`速度慢`,性能较差,对电脑配置有一定要求,但是`质量更可靠`。
|
||||||
|
|
||||||
|
可以修改 `config.toml` 配置文件中的 `subtitle_provider` 进行切换
|
||||||
|
|
||||||
|
建议使用 `edge` 模式,如果生成的字幕质量不好,再切换到 `whisper` 模式
|
||||||
|
|
||||||
|
> 注意:
|
||||||
|
|
||||||
|
1. whisper 模式下需要到 HuggingFace 下载一个模型文件,大约 3GB 左右,请确保网络通畅
|
||||||
|
2. 如果留空,表示不生成字幕。
|
||||||
|
|
||||||
|
> 由于国内无法访问 HuggingFace,可以使用以下方法下载 `whisper-large-v3` 的模型文件
|
||||||
|
|
||||||
|
下载地址:
|
||||||
|
|
||||||
|
- 百度网盘: https://pan.baidu.com/s/11h3Q6tsDtjQKTjUu3sc5cA?pwd=xjs9
|
||||||
|
- 夸克网盘:https://pan.quark.cn/s/3ee3d991d64b
|
||||||
|
|
||||||
|
模型下载后解压,整个目录放到 `.\MoneyPrinterTurbo\models` 里面,
|
||||||
|
最终的文件路径应该是这样: `.\MoneyPrinterTurbo\models\whisper-large-v3`
|
||||||
|
|
||||||
|
```
|
||||||
|
MoneyPrinterTurbo
|
||||||
|
├─models
|
||||||
|
│ └─whisper-large-v3
|
||||||
|
│ config.json
|
||||||
|
│ model.bin
|
||||||
|
│ preprocessor_config.json
|
||||||
|
│ tokenizer.json
|
||||||
|
│ vocabulary.json
|
||||||
|
```
|
||||||
7
sites/docs/zh/guide/thanks-for-sponsoring.md
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
## 感谢赞助 🙏
|
||||||
|
|
||||||
|
感谢佐糖 https://picwish.cn 对该项目的支持和赞助,使得该项目能够持续的更新和维护。
|
||||||
|
|
||||||
|
佐糖专注于**图像处理领域**,提供丰富的**图像处理工具**,将复杂操作极致简化,真正实现让图像处理更简单。
|
||||||
|
|
||||||
|

|
||||||
37
sites/docs/zh/guide/video-demonstration.md
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
## 视频演示 📺
|
||||||
|
|
||||||
|
### 竖屏 9:16
|
||||||
|
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th align="center"><g-emoji class="g-emoji" alias="arrow_forward">▶️</g-emoji> 《如何增加生活的乐趣》</th>
|
||||||
|
<th align="center"><g-emoji class="g-emoji" alias="arrow_forward">▶️</g-emoji> 《金钱的作用》<br>更真实的合成声音</th>
|
||||||
|
<th align="center"><g-emoji class="g-emoji" alias="arrow_forward">▶️</g-emoji> 《生命的意义是什么》</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td align="center"><video src="https://github.com/harry0703/MoneyPrinterTurbo/assets/4928832/a84d33d5-27a2-4aba-8fd0-9fb2bd91c6a6"></video></td>
|
||||||
|
<td align="center"><video src="https://github.com/harry0703/MoneyPrinterTurbo/assets/4928832/af2f3b0b-002e-49fe-b161-18ba91c055e8"></video></td>
|
||||||
|
<td align="center"><video src="https://github.com/harry0703/MoneyPrinterTurbo/assets/4928832/112c9564-d52b-4472-99ad-970b75f66476"></video></td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
### 横屏 16:9
|
||||||
|
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th align="center"><g-emoji class="g-emoji" alias="arrow_forward">▶️</g-emoji>《生命的意义是什么》</th>
|
||||||
|
<th align="center"><g-emoji class="g-emoji" alias="arrow_forward">▶️</g-emoji>《为什么要运动》</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td align="center"><video src="https://github.com/harry0703/MoneyPrinterTurbo/assets/4928832/346ebb15-c55f-47a9-a653-114f08bb8073"></video></td>
|
||||||
|
<td align="center"><video src="https://github.com/harry0703/MoneyPrinterTurbo/assets/4928832/271f2fae-8283-44a0-8aa0-0ed8f9a6fa87"></video></td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
24
sites/package.json
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
{
|
||||||
|
"name": "MoneyPrinterTurbo",
|
||||||
|
"version": "1.1.2",
|
||||||
|
"description": "利用AI大模型,一键生成高清短视频 Generate short videos with one click using AI LLM.",
|
||||||
|
"main": "index.js",
|
||||||
|
"repository": "https://github.com/harry0703/MoneyPrinterTurbo",
|
||||||
|
"author": "harry0703",
|
||||||
|
"license": "MIT",
|
||||||
|
"devDependencies": {
|
||||||
|
"@vue/tsconfig": "^0.1.3",
|
||||||
|
"@vuepress/bundler-vite": "2.0.0-rc.9",
|
||||||
|
"@vuepress/theme-default": "2.0.0-rc.25",
|
||||||
|
"gh-pages": "^6.1.1",
|
||||||
|
"vue": "^3.4.23",
|
||||||
|
"vue-router": "^4.3.1",
|
||||||
|
"vuepress": "2.0.0-rc.9"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"docs:dev": "vuepress dev docs",
|
||||||
|
"docs:build": "vuepress build docs",
|
||||||
|
"predeploy": "pnpm docs:build",
|
||||||
|
"deploy": "gh-pages -d docs/.vuepress/dist"
|
||||||
|
}
|
||||||
|
}
|
||||||
2284
sites/pnpm-lock.yaml
generated
Normal file
14
sites/tsconfig.config.json
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
{
|
||||||
|
"extends": "@vue/tsconfig/tsconfig.node.json",
|
||||||
|
"include": [
|
||||||
|
"vite.config.*",
|
||||||
|
"vitest.config.*",
|
||||||
|
"cypress.config.*"
|
||||||
|
],
|
||||||
|
"compilerOptions": {
|
||||||
|
"composite": true,
|
||||||
|
"types": [
|
||||||
|
"node"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
24
sites/tsconfig.json
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
{
|
||||||
|
"extends": "@vue/tsconfig/tsconfig.web.json",
|
||||||
|
"module": "esnext",
|
||||||
|
"include": [
|
||||||
|
"env.d.ts",
|
||||||
|
"src/**/*",
|
||||||
|
"src/**/*.vue",
|
||||||
|
"docs/.vuepress/*.ts"
|
||||||
|
],
|
||||||
|
"compilerOptions": {
|
||||||
|
"baseUrl": ".",
|
||||||
|
"isolatedModules": true,
|
||||||
|
"paths": {
|
||||||
|
"@/*": [
|
||||||
|
"./src/*"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"references": [
|
||||||
|
{
|
||||||
|
"path": "./tsconfig.config.json"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -1,2 +1,7 @@
|
|||||||
|
@echo off
|
||||||
|
set CURRENT_DIR=%CD%
|
||||||
|
echo ***** Current directory: %CURRENT_DIR% *****
|
||||||
|
set PYTHONPATH=%CURRENT_DIR%
|
||||||
|
|
||||||
rem set HF_ENDPOINT=https://hf-mirror.com
|
rem set HF_ENDPOINT=https://hf-mirror.com
|
||||||
streamlit run .\webui\Main.py --browser.gatherUsageStats=False --server.enableCORS=True
|
streamlit run .\webui\Main.py --browser.gatherUsageStats=False --server.enableCORS=True
|
||||||
269
webui/Main.py
@@ -1,5 +1,6 @@
|
|||||||
import sys
|
import sys
|
||||||
import os
|
import os
|
||||||
|
import time
|
||||||
|
|
||||||
# Add the root directory of the project to the system path to allow importing modules from the project
|
# Add the root directory of the project to the system path to allow importing modules from the project
|
||||||
root_dir = os.path.dirname(os.path.dirname(os.path.realpath(__file__)))
|
root_dir = os.path.dirname(os.path.dirname(os.path.realpath(__file__)))
|
||||||
@@ -29,10 +30,11 @@ st.set_page_config(page_title="MoneyPrinterTurbo",
|
|||||||
"video.\n\nhttps://github.com/harry0703/MoneyPrinterTurbo"
|
"video.\n\nhttps://github.com/harry0703/MoneyPrinterTurbo"
|
||||||
})
|
})
|
||||||
|
|
||||||
from app.models.schema import VideoParams, VideoAspect, VideoConcatMode
|
from app.models.schema import VideoParams, VideoAspect, VideoConcatMode, MaterialInfo
|
||||||
from app.services import task as tm, llm, voice
|
from app.services import task as tm, llm, voice
|
||||||
from app.utils import utils
|
from app.utils import utils
|
||||||
from app.config import config
|
from app.config import config
|
||||||
|
from app.models.const import FILE_TYPE_VIDEOS, FILE_TYPE_IMAGES
|
||||||
|
|
||||||
hide_streamlit_style = """
|
hide_streamlit_style = """
|
||||||
<style>#root > div:nth-child(1) > div > div > div > div > section > div {padding-top: 0rem;}</style>
|
<style>#root > div:nth-child(1) > div > div > div > div > section > div {padding-top: 0rem;}</style>
|
||||||
@@ -40,11 +42,14 @@ hide_streamlit_style = """
|
|||||||
st.markdown(hide_streamlit_style, unsafe_allow_html=True)
|
st.markdown(hide_streamlit_style, unsafe_allow_html=True)
|
||||||
st.title(f"MoneyPrinterTurbo v{config.project_version}")
|
st.title(f"MoneyPrinterTurbo v{config.project_version}")
|
||||||
|
|
||||||
|
support_locales = ["zh-CN", "zh-HK", "zh-TW", "de-DE", "en-US", "fr-FR", "vi-VN", "th-TH"]
|
||||||
|
|
||||||
font_dir = os.path.join(root_dir, "resource", "fonts")
|
font_dir = os.path.join(root_dir, "resource", "fonts")
|
||||||
song_dir = os.path.join(root_dir, "resource", "songs")
|
song_dir = os.path.join(root_dir, "resource", "songs")
|
||||||
i18n_dir = os.path.join(root_dir, "webui", "i18n")
|
i18n_dir = os.path.join(root_dir, "webui", "i18n")
|
||||||
config_file = os.path.join(root_dir, "webui", ".streamlit", "webui.toml")
|
config_file = os.path.join(root_dir, "webui", ".streamlit", "webui.toml")
|
||||||
system_locale = utils.get_system_locale()
|
system_locale = utils.get_system_locale()
|
||||||
|
# print(f"******** system locale: {system_locale} ********")
|
||||||
|
|
||||||
if 'video_subject' not in st.session_state:
|
if 'video_subject' not in st.session_state:
|
||||||
st.session_state['video_subject'] = ''
|
st.session_state['video_subject'] = ''
|
||||||
@@ -62,6 +67,7 @@ def get_all_fonts():
|
|||||||
for file in files:
|
for file in files:
|
||||||
if file.endswith(".ttf") or file.endswith(".ttc"):
|
if file.endswith(".ttf") or file.endswith(".ttc"):
|
||||||
fonts.append(file)
|
fonts.append(file)
|
||||||
|
fonts.sort()
|
||||||
return fonts
|
return fonts
|
||||||
|
|
||||||
|
|
||||||
@@ -145,7 +151,10 @@ def tr(key):
|
|||||||
|
|
||||||
st.write(tr("Get Help"))
|
st.write(tr("Get Help"))
|
||||||
|
|
||||||
with st.expander(tr("Basic Settings"), expanded=False):
|
llm_provider = config.app.get("llm_provider", "").lower()
|
||||||
|
|
||||||
|
if not config.app.get("hide_config", False):
|
||||||
|
with st.expander(tr("Basic Settings"), expanded=False):
|
||||||
config_panels = st.columns(3)
|
config_panels = st.columns(3)
|
||||||
left_config_panel = config_panels[0]
|
left_config_panel = config_panels[0]
|
||||||
middle_config_panel = config_panels[1]
|
middle_config_panel = config_panels[1]
|
||||||
@@ -164,7 +173,6 @@ with st.expander(tr("Basic Settings"), expanded=False):
|
|||||||
code = selected_language.split(" - ")[0].strip()
|
code = selected_language.split(" - ")[0].strip()
|
||||||
st.session_state['ui_language'] = code
|
st.session_state['ui_language'] = code
|
||||||
config.ui['language'] = code
|
config.ui['language'] = code
|
||||||
config.save_config()
|
|
||||||
|
|
||||||
with middle_config_panel:
|
with middle_config_panel:
|
||||||
# openai
|
# openai
|
||||||
@@ -175,7 +183,8 @@ with st.expander(tr("Basic Settings"), expanded=False):
|
|||||||
# qwen (通义千问)
|
# qwen (通义千问)
|
||||||
# gemini
|
# gemini
|
||||||
# ollama
|
# ollama
|
||||||
llm_providers = ['OpenAI', 'Moonshot', 'Azure', 'Qwen', 'Gemini', 'Ollama', 'G4f', 'OneAPI']
|
llm_providers = ['OpenAI', 'Moonshot', 'Azure', 'Qwen', 'DeepSeek', 'Gemini', 'Ollama', 'G4f', 'OneAPI',
|
||||||
|
"Cloudflare"]
|
||||||
saved_llm_provider = config.app.get("llm_provider", "OpenAI").lower()
|
saved_llm_provider = config.app.get("llm_provider", "OpenAI").lower()
|
||||||
saved_llm_provider_index = 0
|
saved_llm_provider_index = 0
|
||||||
for i, provider in enumerate(llm_providers):
|
for i, provider in enumerate(llm_providers):
|
||||||
@@ -184,15 +193,122 @@ with st.expander(tr("Basic Settings"), expanded=False):
|
|||||||
break
|
break
|
||||||
|
|
||||||
llm_provider = st.selectbox(tr("LLM Provider"), options=llm_providers, index=saved_llm_provider_index)
|
llm_provider = st.selectbox(tr("LLM Provider"), options=llm_providers, index=saved_llm_provider_index)
|
||||||
|
llm_helper = st.container()
|
||||||
llm_provider = llm_provider.lower()
|
llm_provider = llm_provider.lower()
|
||||||
config.app["llm_provider"] = llm_provider
|
config.app["llm_provider"] = llm_provider
|
||||||
|
|
||||||
llm_api_key = config.app.get(f"{llm_provider}_api_key", "")
|
llm_api_key = config.app.get(f"{llm_provider}_api_key", "")
|
||||||
llm_base_url = config.app.get(f"{llm_provider}_base_url", "")
|
llm_base_url = config.app.get(f"{llm_provider}_base_url", "")
|
||||||
llm_model_name = config.app.get(f"{llm_provider}_model_name", "")
|
llm_model_name = config.app.get(f"{llm_provider}_model_name", "")
|
||||||
|
llm_account_id = config.app.get(f"{llm_provider}_account_id", "")
|
||||||
|
|
||||||
|
tips = ""
|
||||||
|
if llm_provider == 'ollama':
|
||||||
|
if not llm_model_name:
|
||||||
|
llm_model_name = "qwen:7b"
|
||||||
|
if not llm_base_url:
|
||||||
|
llm_base_url = "http://localhost:11434/v1"
|
||||||
|
|
||||||
|
with llm_helper:
|
||||||
|
tips = """
|
||||||
|
##### Ollama配置说明
|
||||||
|
- **API Key**: 随便填写,比如 123
|
||||||
|
- **Base Url**: 一般为 http://localhost:11434/v1
|
||||||
|
- 如果 `MoneyPrinterTurbo` 和 `Ollama` **不在同一台机器上**,需要填写 `Ollama` 机器的IP地址
|
||||||
|
- 如果 `MoneyPrinterTurbo` 是 `Docker` 部署,建议填写 `http://host.docker.internal:11434/v1`
|
||||||
|
- **Model Name**: 使用 `ollama list` 查看,比如 `qwen:7b`
|
||||||
|
"""
|
||||||
|
|
||||||
|
if llm_provider == 'openai':
|
||||||
|
if not llm_model_name:
|
||||||
|
llm_model_name = "gpt-3.5-turbo"
|
||||||
|
with llm_helper:
|
||||||
|
tips = """
|
||||||
|
##### OpenAI 配置说明
|
||||||
|
> 需要VPN开启全局流量模式
|
||||||
|
- **API Key**: [点击到官网申请](https://platform.openai.com/api-keys)
|
||||||
|
- **Base Url**: 可以留空
|
||||||
|
- **Model Name**: 填写**有权限**的模型,[点击查看模型列表](https://platform.openai.com/settings/organization/limits)
|
||||||
|
"""
|
||||||
|
|
||||||
|
if llm_provider == 'moonshot':
|
||||||
|
if not llm_model_name:
|
||||||
|
llm_model_name = "moonshot-v1-8k"
|
||||||
|
with llm_helper:
|
||||||
|
tips = """
|
||||||
|
##### Moonshot 配置说明
|
||||||
|
- **API Key**: [点击到官网申请](https://platform.moonshot.cn/console/api-keys)
|
||||||
|
- **Base Url**: 固定为 https://api.moonshot.cn/v1
|
||||||
|
- **Model Name**: 比如 moonshot-v1-8k,[点击查看模型列表](https://platform.moonshot.cn/docs/intro#%E6%A8%A1%E5%9E%8B%E5%88%97%E8%A1%A8)
|
||||||
|
"""
|
||||||
|
|
||||||
|
if llm_provider == 'qwen':
|
||||||
|
if not llm_model_name:
|
||||||
|
llm_model_name = "qwen-max"
|
||||||
|
with llm_helper:
|
||||||
|
tips = """
|
||||||
|
##### 通义千问Qwen 配置说明
|
||||||
|
- **API Key**: [点击到官网申请](https://dashscope.console.aliyun.com/apiKey)
|
||||||
|
- **Base Url**: 留空
|
||||||
|
- **Model Name**: 比如 qwen-max,[点击查看模型列表](https://help.aliyun.com/zh/dashscope/developer-reference/model-introduction#3ef6d0bcf91wy)
|
||||||
|
"""
|
||||||
|
|
||||||
|
if llm_provider == 'g4f':
|
||||||
|
if not llm_model_name:
|
||||||
|
llm_model_name = "gpt-3.5-turbo"
|
||||||
|
with llm_helper:
|
||||||
|
tips = """
|
||||||
|
##### gpt4free 配置说明
|
||||||
|
> [GitHub开源项目](https://github.com/xtekky/gpt4free),可以免费使用GPT模型,但是**稳定性较差**
|
||||||
|
- **API Key**: 随便填写,比如 123
|
||||||
|
- **Base Url**: 留空
|
||||||
|
- **Model Name**: 比如 gpt-3.5-turbo,[点击查看模型列表](https://github.com/xtekky/gpt4free/blob/main/g4f/models.py#L308)
|
||||||
|
"""
|
||||||
|
if llm_provider == 'azure':
|
||||||
|
with llm_helper:
|
||||||
|
tips = """
|
||||||
|
##### Azure 配置说明
|
||||||
|
> [点击查看如何部署模型](https://learn.microsoft.com/zh-cn/azure/ai-services/openai/how-to/create-resource)
|
||||||
|
- **API Key**: [点击到Azure后台创建](https://portal.azure.com/#view/Microsoft_Azure_ProjectOxford/CognitiveServicesHub/~/OpenAI)
|
||||||
|
- **Base Url**: 留空
|
||||||
|
- **Model Name**: 填写你实际的部署名
|
||||||
|
"""
|
||||||
|
|
||||||
|
if llm_provider == 'gemini':
|
||||||
|
if not llm_model_name:
|
||||||
|
llm_model_name = "gemini-1.0-pro"
|
||||||
|
|
||||||
|
with llm_helper:
|
||||||
|
tips = """
|
||||||
|
##### Gemini 配置说明
|
||||||
|
> 需要VPN开启全局流量模式
|
||||||
|
- **API Key**: [点击到官网申请](https://ai.google.dev/)
|
||||||
|
- **Base Url**: 留空
|
||||||
|
- **Model Name**: 比如 gemini-1.0-pro
|
||||||
|
"""
|
||||||
|
|
||||||
|
if llm_provider == 'deepseek':
|
||||||
|
if not llm_model_name:
|
||||||
|
llm_model_name = "deepseek-chat"
|
||||||
|
if not llm_base_url:
|
||||||
|
llm_base_url = "https://api.deepseek.com"
|
||||||
|
with llm_helper:
|
||||||
|
tips = """
|
||||||
|
##### DeepSeek 配置说明
|
||||||
|
- **API Key**: [点击到官网申请](https://platform.deepseek.com/api_keys)
|
||||||
|
- **Base Url**: 固定为 https://api.deepseek.com
|
||||||
|
- **Model Name**: 固定为 deepseek-chat
|
||||||
|
"""
|
||||||
|
|
||||||
|
if tips and config.ui['language'] == 'zh':
|
||||||
|
st.warning(
|
||||||
|
"中国用户建议使用 **DeepSeek** 或 **Moonshot** 作为大模型提供商\n- 国内可直接访问,不需要VPN \n- 注册就送额度,基本够用")
|
||||||
|
st.info(tips)
|
||||||
|
|
||||||
st_llm_api_key = st.text_input(tr("API Key"), value=llm_api_key, type="password")
|
st_llm_api_key = st.text_input(tr("API Key"), value=llm_api_key, type="password")
|
||||||
st_llm_base_url = st.text_input(tr("Base Url"), value=llm_base_url)
|
st_llm_base_url = st.text_input(tr("Base Url"), value=llm_base_url)
|
||||||
st_llm_model_name = st.text_input(tr("Model Name"), value=llm_model_name)
|
st_llm_model_name = st.text_input(tr("Model Name"), value=llm_model_name)
|
||||||
|
|
||||||
if st_llm_api_key:
|
if st_llm_api_key:
|
||||||
config.app[f"{llm_provider}_api_key"] = st_llm_api_key
|
config.app[f"{llm_provider}_api_key"] = st_llm_api_key
|
||||||
if st_llm_base_url:
|
if st_llm_base_url:
|
||||||
@@ -200,26 +316,41 @@ with st.expander(tr("Basic Settings"), expanded=False):
|
|||||||
if st_llm_model_name:
|
if st_llm_model_name:
|
||||||
config.app[f"{llm_provider}_model_name"] = st_llm_model_name
|
config.app[f"{llm_provider}_model_name"] = st_llm_model_name
|
||||||
|
|
||||||
config.save_config()
|
if llm_provider == 'cloudflare':
|
||||||
|
st_llm_account_id = st.text_input(tr("Account ID"), value=llm_account_id)
|
||||||
|
if st_llm_account_id:
|
||||||
|
config.app[f"{llm_provider}_account_id"] = st_llm_account_id
|
||||||
|
|
||||||
with right_config_panel:
|
with right_config_panel:
|
||||||
pexels_api_keys = config.app.get("pexels_api_keys", [])
|
def get_keys_from_config(cfg_key):
|
||||||
if isinstance(pexels_api_keys, str):
|
api_keys = config.app.get(cfg_key, [])
|
||||||
pexels_api_keys = [pexels_api_keys]
|
if isinstance(api_keys, str):
|
||||||
pexels_api_key = ", ".join(pexels_api_keys)
|
api_keys = [api_keys]
|
||||||
|
api_key = ", ".join(api_keys)
|
||||||
|
return api_key
|
||||||
|
|
||||||
|
|
||||||
|
def save_keys_to_config(cfg_key, value):
|
||||||
|
value = value.replace(" ", "")
|
||||||
|
if value:
|
||||||
|
config.app[cfg_key] = value.split(",")
|
||||||
|
|
||||||
|
|
||||||
|
pexels_api_key = get_keys_from_config("pexels_api_keys")
|
||||||
pexels_api_key = st.text_input(tr("Pexels API Key"), value=pexels_api_key, type="password")
|
pexels_api_key = st.text_input(tr("Pexels API Key"), value=pexels_api_key, type="password")
|
||||||
pexels_api_key = pexels_api_key.replace(" ", "")
|
save_keys_to_config("pexels_api_keys", pexels_api_key)
|
||||||
if pexels_api_key:
|
|
||||||
config.app["pexels_api_keys"] = pexels_api_key.split(",")
|
pixabay_api_key = get_keys_from_config("pixabay_api_keys")
|
||||||
config.save_config()
|
pixabay_api_key = st.text_input(tr("Pixabay API Key"), value=pixabay_api_key, type="password")
|
||||||
|
save_keys_to_config("pixabay_api_keys", pixabay_api_key)
|
||||||
|
|
||||||
panel = st.columns(3)
|
panel = st.columns(3)
|
||||||
left_panel = panel[0]
|
left_panel = panel[0]
|
||||||
middle_panel = panel[1]
|
middle_panel = panel[1]
|
||||||
right_panel = panel[2]
|
right_panel = panel[2]
|
||||||
|
|
||||||
params = VideoParams()
|
params = VideoParams(video_subject="")
|
||||||
|
uploaded_files = []
|
||||||
|
|
||||||
with left_panel:
|
with left_panel:
|
||||||
with st.container(border=True):
|
with st.container(border=True):
|
||||||
@@ -230,7 +361,7 @@ with left_panel:
|
|||||||
video_languages = [
|
video_languages = [
|
||||||
(tr("Auto Detect"), ""),
|
(tr("Auto Detect"), ""),
|
||||||
]
|
]
|
||||||
for code in ["zh-CN", "zh-TW", "de-DE", "en-US"]:
|
for code in support_locales:
|
||||||
video_languages.append((code, code))
|
video_languages.append((code, code))
|
||||||
|
|
||||||
selected_index = st.selectbox(tr("Script Language"),
|
selected_index = st.selectbox(tr("Script Language"),
|
||||||
@@ -273,6 +404,32 @@ with middle_panel:
|
|||||||
(tr("Sequential"), "sequential"),
|
(tr("Sequential"), "sequential"),
|
||||||
(tr("Random"), "random"),
|
(tr("Random"), "random"),
|
||||||
]
|
]
|
||||||
|
video_sources = [
|
||||||
|
(tr("Pexels"), "pexels"),
|
||||||
|
(tr("Pixabay"), "pixabay"),
|
||||||
|
(tr("Local file"), "local"),
|
||||||
|
(tr("TikTok"), "douyin"),
|
||||||
|
(tr("Bilibili"), "bilibili"),
|
||||||
|
(tr("Xiaohongshu"), "xiaohongshu"),
|
||||||
|
]
|
||||||
|
|
||||||
|
saved_video_source_name = config.app.get("video_source", "pexels")
|
||||||
|
saved_video_source_index = [v[1] for v in video_sources].index(saved_video_source_name)
|
||||||
|
|
||||||
|
selected_index = st.selectbox(tr("Video Source"),
|
||||||
|
options=range(len(video_sources)),
|
||||||
|
format_func=lambda x: video_sources[x][0],
|
||||||
|
index=saved_video_source_index
|
||||||
|
)
|
||||||
|
params.video_source = video_sources[selected_index][1]
|
||||||
|
config.app["video_source"] = params.video_source
|
||||||
|
|
||||||
|
if params.video_source == 'local':
|
||||||
|
_supported_types = FILE_TYPE_VIDEOS + FILE_TYPE_IMAGES
|
||||||
|
uploaded_files = st.file_uploader("Upload Local Files",
|
||||||
|
type=["mp4", "mov", "avi", "flv", "mkv", "jpg", "jpeg", "png"],
|
||||||
|
accept_multiple_files=True)
|
||||||
|
|
||||||
selected_index = st.selectbox(tr("Video Concat Mode"),
|
selected_index = st.selectbox(tr("Video Concat Mode"),
|
||||||
index=1,
|
index=1,
|
||||||
options=range(len(video_concat_modes)), # 使用索引作为内部选项值
|
options=range(len(video_concat_modes)), # 使用索引作为内部选项值
|
||||||
@@ -295,20 +452,25 @@ with middle_panel:
|
|||||||
index=0)
|
index=0)
|
||||||
with st.container(border=True):
|
with st.container(border=True):
|
||||||
st.write(tr("Audio Settings"))
|
st.write(tr("Audio Settings"))
|
||||||
voices = voice.get_all_voices(filter_locals=["zh-CN", "zh-HK", "zh-TW", "de-DE", "en-US"])
|
|
||||||
|
# tts_providers = ['edge', 'azure']
|
||||||
|
# tts_provider = st.selectbox(tr("TTS Provider"), tts_providers)
|
||||||
|
|
||||||
|
voices = voice.get_all_azure_voices(
|
||||||
|
filter_locals=support_locales)
|
||||||
friendly_names = {
|
friendly_names = {
|
||||||
voice: voice.
|
v: v.
|
||||||
replace("Female", tr("Female")).
|
replace("Female", tr("Female")).
|
||||||
replace("Male", tr("Male")).
|
replace("Male", tr("Male")).
|
||||||
replace("Neural", "") for
|
replace("Neural", "") for
|
||||||
voice in voices}
|
v in voices}
|
||||||
saved_voice_name = config.ui.get("voice_name", "")
|
saved_voice_name = config.ui.get("voice_name", "")
|
||||||
saved_voice_name_index = 0
|
saved_voice_name_index = 0
|
||||||
if saved_voice_name in friendly_names:
|
if saved_voice_name in friendly_names:
|
||||||
saved_voice_name_index = list(friendly_names.keys()).index(saved_voice_name)
|
saved_voice_name_index = list(friendly_names.keys()).index(saved_voice_name)
|
||||||
else:
|
else:
|
||||||
for i, voice in enumerate(voices):
|
for i, v in enumerate(voices):
|
||||||
if voice.lower().startswith(st.session_state['ui_language'].lower()):
|
if v.lower().startswith(st.session_state['ui_language'].lower()) and "V2" not in v:
|
||||||
saved_voice_name_index = i
|
saved_voice_name_index = i
|
||||||
break
|
break
|
||||||
|
|
||||||
@@ -319,7 +481,34 @@ with middle_panel:
|
|||||||
voice_name = list(friendly_names.keys())[list(friendly_names.values()).index(selected_friendly_name)]
|
voice_name = list(friendly_names.keys())[list(friendly_names.values()).index(selected_friendly_name)]
|
||||||
params.voice_name = voice_name
|
params.voice_name = voice_name
|
||||||
config.ui['voice_name'] = voice_name
|
config.ui['voice_name'] = voice_name
|
||||||
config.save_config()
|
|
||||||
|
if st.button(tr("Play Voice")):
|
||||||
|
play_content = params.video_subject
|
||||||
|
if not play_content:
|
||||||
|
play_content = params.video_script
|
||||||
|
if not play_content:
|
||||||
|
play_content = tr("Voice Example")
|
||||||
|
with st.spinner(tr("Synthesizing Voice")):
|
||||||
|
temp_dir = utils.storage_dir("temp", create=True)
|
||||||
|
audio_file = os.path.join(temp_dir, f"tmp-voice-{str(uuid4())}.mp3")
|
||||||
|
sub_maker = voice.tts(text=play_content, voice_name=voice_name, voice_file=audio_file)
|
||||||
|
# if the voice file generation failed, try again with a default content.
|
||||||
|
if not sub_maker:
|
||||||
|
play_content = "This is a example voice. if you hear this, the voice synthesis failed with the original content."
|
||||||
|
sub_maker = voice.tts(text=play_content, voice_name=voice_name, voice_file=audio_file)
|
||||||
|
|
||||||
|
if sub_maker and os.path.exists(audio_file):
|
||||||
|
st.audio(audio_file, format="audio/mp3")
|
||||||
|
if os.path.exists(audio_file):
|
||||||
|
os.remove(audio_file)
|
||||||
|
|
||||||
|
if voice.is_azure_v2_voice(voice_name):
|
||||||
|
saved_azure_speech_region = config.azure.get(f"speech_region", "")
|
||||||
|
saved_azure_speech_key = config.azure.get(f"speech_key", "")
|
||||||
|
azure_speech_region = st.text_input(tr("Speech Region"), value=saved_azure_speech_region)
|
||||||
|
azure_speech_key = st.text_input(tr("Speech Key"), value=saved_azure_speech_key, type="password")
|
||||||
|
config.azure["speech_region"] = azure_speech_region
|
||||||
|
config.azure["speech_key"] = azure_speech_key
|
||||||
|
|
||||||
params.voice_volume = st.selectbox(tr("Speech Volume"),
|
params.voice_volume = st.selectbox(tr("Speech Volume"),
|
||||||
options=[0.6, 0.8, 1.0, 1.2, 1.5, 2.0, 3.0, 4.0, 5.0], index=2)
|
options=[0.6, 0.8, 1.0, 1.2, 1.5, 2.0, 3.0, 4.0, 5.0], index=2)
|
||||||
@@ -334,10 +523,10 @@ with middle_panel:
|
|||||||
format_func=lambda x: bgm_options[x][0] # 显示给用户的是标签
|
format_func=lambda x: bgm_options[x][0] # 显示给用户的是标签
|
||||||
)
|
)
|
||||||
# 获取选择的背景音乐类型
|
# 获取选择的背景音乐类型
|
||||||
bgm_type = bgm_options[selected_index][1]
|
params.bgm_type = bgm_options[selected_index][1]
|
||||||
|
|
||||||
# 根据选择显示或隐藏组件
|
# 根据选择显示或隐藏组件
|
||||||
if bgm_type == "custom":
|
if params.bgm_type == "custom":
|
||||||
custom_bgm_file = st.text_input(tr("Custom Background Music File"))
|
custom_bgm_file = st.text_input(tr("Custom Background Music File"))
|
||||||
if custom_bgm_file and os.path.exists(custom_bgm_file):
|
if custom_bgm_file and os.path.exists(custom_bgm_file):
|
||||||
params.bgm_file = custom_bgm_file
|
params.bgm_file = custom_bgm_file
|
||||||
@@ -356,7 +545,6 @@ with right_panel:
|
|||||||
saved_font_name_index = font_names.index(saved_font_name)
|
saved_font_name_index = font_names.index(saved_font_name)
|
||||||
params.font_name = st.selectbox(tr("Font"), font_names, index=saved_font_name_index)
|
params.font_name = st.selectbox(tr("Font"), font_names, index=saved_font_name_index)
|
||||||
config.ui['font_name'] = params.font_name
|
config.ui['font_name'] = params.font_name
|
||||||
config.save_config()
|
|
||||||
|
|
||||||
subtitle_positions = [
|
subtitle_positions = [
|
||||||
(tr("Top"), "top"),
|
(tr("Top"), "top"),
|
||||||
@@ -401,11 +589,34 @@ if start_button:
|
|||||||
scroll_to_bottom()
|
scroll_to_bottom()
|
||||||
st.stop()
|
st.stop()
|
||||||
|
|
||||||
if not config.app.get("pexels_api_keys", ""):
|
if params.video_source not in ["pexels", "pixabay", "local"]:
|
||||||
|
st.error(tr("Please Select a Valid Video Source"))
|
||||||
|
scroll_to_bottom()
|
||||||
|
st.stop()
|
||||||
|
|
||||||
|
if params.video_source == "pexels" and not config.app.get("pexels_api_keys", ""):
|
||||||
st.error(tr("Please Enter the Pexels API Key"))
|
st.error(tr("Please Enter the Pexels API Key"))
|
||||||
scroll_to_bottom()
|
scroll_to_bottom()
|
||||||
st.stop()
|
st.stop()
|
||||||
|
|
||||||
|
if params.video_source == "pixabay" and not config.app.get("pixabay_api_keys", ""):
|
||||||
|
st.error(tr("Please Enter the Pixabay API Key"))
|
||||||
|
scroll_to_bottom()
|
||||||
|
st.stop()
|
||||||
|
|
||||||
|
if uploaded_files:
|
||||||
|
local_videos_dir = utils.storage_dir("local_videos", create=True)
|
||||||
|
for file in uploaded_files:
|
||||||
|
file_path = os.path.join(local_videos_dir, f"{file.file_id}_{file.name}")
|
||||||
|
with open(file_path, "wb") as f:
|
||||||
|
f.write(file.getbuffer())
|
||||||
|
m = MaterialInfo()
|
||||||
|
m.provider = "local"
|
||||||
|
m.url = file_path
|
||||||
|
if not params.video_materials:
|
||||||
|
params.video_materials = []
|
||||||
|
params.video_materials.append(m)
|
||||||
|
|
||||||
log_container = st.empty()
|
log_container = st.empty()
|
||||||
log_records = []
|
log_records = []
|
||||||
|
|
||||||
@@ -424,12 +635,16 @@ if start_button:
|
|||||||
scroll_to_bottom()
|
scroll_to_bottom()
|
||||||
|
|
||||||
result = tm.start(task_id=task_id, params=params)
|
result = tm.start(task_id=task_id, params=params)
|
||||||
|
if not result or "videos" not in result:
|
||||||
|
st.error(tr("Video Generation Failed"))
|
||||||
|
logger.error(tr("Video Generation Failed"))
|
||||||
|
scroll_to_bottom()
|
||||||
|
st.stop()
|
||||||
|
|
||||||
video_files = result.get("videos", [])
|
video_files = result.get("videos", [])
|
||||||
st.success(tr("Video Generation Completed"))
|
st.success(tr("Video Generation Completed"))
|
||||||
try:
|
try:
|
||||||
if video_files:
|
if video_files:
|
||||||
# center the video player
|
|
||||||
player_cols = st.columns(len(video_files) * 2 + 1)
|
player_cols = st.columns(len(video_files) * 2 + 1)
|
||||||
for i, url in enumerate(video_files):
|
for i, url in enumerate(video_files):
|
||||||
player_cols[i * 2 + 1].video(url)
|
player_cols[i * 2 + 1].video(url)
|
||||||
@@ -439,3 +654,5 @@ if start_button:
|
|||||||
open_task_folder(task_id)
|
open_task_folder(task_id)
|
||||||
logger.info(tr("Video Generation Completed"))
|
logger.info(tr("Video Generation Completed"))
|
||||||
scroll_to_bottom()
|
scroll_to_bottom()
|
||||||
|
|
||||||
|
config.save_config()
|
||||||
|
|||||||
@@ -23,6 +23,8 @@
|
|||||||
"Number of Videos Generated Simultaneously": "Anzahl der parallel generierten Videos",
|
"Number of Videos Generated Simultaneously": "Anzahl der parallel generierten Videos",
|
||||||
"Audio Settings": "**Audio Einstellungen**",
|
"Audio Settings": "**Audio Einstellungen**",
|
||||||
"Speech Synthesis": "Sprachausgabe",
|
"Speech Synthesis": "Sprachausgabe",
|
||||||
|
"Speech Region": "Region(:red[Required,[Get Region](https://portal.azure.com/#view/Microsoft_Azure_ProjectOxford/CognitiveServicesHub/~/SpeechServices)])",
|
||||||
|
"Speech Key": "API Key(:red[Required,[Get API Key](https://portal.azure.com/#view/Microsoft_Azure_ProjectOxford/CognitiveServicesHub/~/SpeechServices)])",
|
||||||
"Speech Volume": "Lautstärke der Sprachausgabe",
|
"Speech Volume": "Lautstärke der Sprachausgabe",
|
||||||
"Male": "Männlich",
|
"Male": "Männlich",
|
||||||
"Female": "Weiblich",
|
"Female": "Weiblich",
|
||||||
@@ -48,9 +50,11 @@
|
|||||||
"Generating Video": "Video wird erstellt, bitte warten...",
|
"Generating Video": "Video wird erstellt, bitte warten...",
|
||||||
"Start Generating Video": "Beginne mit der Generierung",
|
"Start Generating Video": "Beginne mit der Generierung",
|
||||||
"Video Generation Completed": "Video erfolgreich generiert",
|
"Video Generation Completed": "Video erfolgreich generiert",
|
||||||
|
"Video Generation Failed": "Video Generierung fehlgeschlagen",
|
||||||
"You can download the generated video from the following links": "Sie können das generierte Video über die folgenden Links herunterladen",
|
"You can download the generated video from the following links": "Sie können das generierte Video über die folgenden Links herunterladen",
|
||||||
"Basic Settings": "**Grunde Instellungen**",
|
"Basic Settings": "**Grunde Instellungen**",
|
||||||
"Pexels API Key": "Pexels API Key (:red[Required] [Get API Key](https://www.pexels.com/api/))",
|
"Pexels API Key": "Pexels API Key ([Get API Key](https://www.pexels.com/api/))",
|
||||||
|
"Pixabay API Key": "Pixabay API Key ([Get API Key](https://pixabay.com/api/docs/#api_search_videos))",
|
||||||
"Language": "Language",
|
"Language": "Language",
|
||||||
"LLM Provider": "LLM Provider",
|
"LLM Provider": "LLM Provider",
|
||||||
"API Key": "API Key (:red[Required])",
|
"API Key": "API Key (:red[Required])",
|
||||||
@@ -58,6 +62,16 @@
|
|||||||
"Model Name": "Model Name",
|
"Model Name": "Model Name",
|
||||||
"Please Enter the LLM API Key": "Please Enter the **LLM API Key**",
|
"Please Enter the LLM API Key": "Please Enter the **LLM API Key**",
|
||||||
"Please Enter the Pexels API Key": "Please Enter the **Pexels API Key**",
|
"Please Enter the Pexels API Key": "Please Enter the **Pexels API Key**",
|
||||||
"Get Help": "If you need help, or have any questions, you can join discord for help: https://harryai.cc"
|
"Please Enter the Pixabay API Key": "Please Enter the **Pixabay API Key**",
|
||||||
|
"Get Help": "If you need help, or have any questions, you can join discord for help: https://harryai.cc",
|
||||||
|
"Video Source": "Video Source",
|
||||||
|
"TikTok": "TikTok (TikTok support is coming soon)",
|
||||||
|
"Bilibili": "Bilibili (Bilibili support is coming soon)",
|
||||||
|
"Xiaohongshu": "Xiaohongshu (Xiaohongshu support is coming soon)",
|
||||||
|
"Local file": "Local file",
|
||||||
|
"Play Voice": "Play Voice",
|
||||||
|
"Voice Example": "This is an example text for testing speech synthesis",
|
||||||
|
"Synthesizing Voice": "Synthesizing voice, please wait...",
|
||||||
|
"TTS Provider": "Select the voice synthesis provider"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -23,6 +23,8 @@
|
|||||||
"Number of Videos Generated Simultaneously": "Number of Videos Generated Simultaneously",
|
"Number of Videos Generated Simultaneously": "Number of Videos Generated Simultaneously",
|
||||||
"Audio Settings": "**Audio Settings**",
|
"Audio Settings": "**Audio Settings**",
|
||||||
"Speech Synthesis": "Speech Synthesis Voice",
|
"Speech Synthesis": "Speech Synthesis Voice",
|
||||||
|
"Speech Region": "Region(:red[Required,[Get Region](https://portal.azure.com/#view/Microsoft_Azure_ProjectOxford/CognitiveServicesHub/~/SpeechServices)])",
|
||||||
|
"Speech Key": "API Key(:red[Required,[Get API Key](https://portal.azure.com/#view/Microsoft_Azure_ProjectOxford/CognitiveServicesHub/~/SpeechServices)])",
|
||||||
"Speech Volume": "Speech Volume (1.0 represents 100%)",
|
"Speech Volume": "Speech Volume (1.0 represents 100%)",
|
||||||
"Male": "Male",
|
"Male": "Male",
|
||||||
"Female": "Female",
|
"Female": "Female",
|
||||||
@@ -48,16 +50,29 @@
|
|||||||
"Generating Video": "Generating video, please wait...",
|
"Generating Video": "Generating video, please wait...",
|
||||||
"Start Generating Video": "Start Generating Video",
|
"Start Generating Video": "Start Generating Video",
|
||||||
"Video Generation Completed": "Video Generation Completed",
|
"Video Generation Completed": "Video Generation Completed",
|
||||||
|
"Video Generation Failed": "Video Generation Failed",
|
||||||
"You can download the generated video from the following links": "You can download the generated video from the following links",
|
"You can download the generated video from the following links": "You can download the generated video from the following links",
|
||||||
"Pexels API Key": "Pexels API Key (:red[Required] [Get API Key](https://www.pexels.com/api/))",
|
"Pexels API Key": "Pexels API Key ([Get API Key](https://www.pexels.com/api/))",
|
||||||
|
"Pixabay API Key": "Pixabay API Key ([Get API Key](https://pixabay.com/api/docs/#api_search_videos))",
|
||||||
"Basic Settings": "**Basic Settings** (:blue[Click to expand])",
|
"Basic Settings": "**Basic Settings** (:blue[Click to expand])",
|
||||||
"Language": "Language",
|
"Language": "Language",
|
||||||
"LLM Provider": "LLM Provider",
|
"LLM Provider": "LLM Provider",
|
||||||
"API Key": "API Key (:red[Required])",
|
"API Key": "API Key (:red[Required])",
|
||||||
"Base Url": "Base Url",
|
"Base Url": "Base Url",
|
||||||
|
"Account ID": "Account ID (Get from Cloudflare dashboard)",
|
||||||
"Model Name": "Model Name",
|
"Model Name": "Model Name",
|
||||||
"Please Enter the LLM API Key": "Please Enter the **LLM API Key**",
|
"Please Enter the LLM API Key": "Please Enter the **LLM API Key**",
|
||||||
"Please Enter the Pexels API Key": "Please Enter the **Pexels API Key**",
|
"Please Enter the Pexels API Key": "Please Enter the **Pexels API Key**",
|
||||||
"Get Help": "If you need help, or have any questions, you can join discord for help: https://harryai.cc"
|
"Please Enter the Pixabay API Key": "Please Enter the **Pixabay API Key**",
|
||||||
|
"Get Help": "If you need help, or have any questions, you can join discord for help: https://harryai.cc",
|
||||||
|
"Video Source": "Video Source",
|
||||||
|
"TikTok": "TikTok (TikTok support is coming soon)",
|
||||||
|
"Bilibili": "Bilibili (Bilibili support is coming soon)",
|
||||||
|
"Xiaohongshu": "Xiaohongshu (Xiaohongshu support is coming soon)",
|
||||||
|
"Local file": "Local file",
|
||||||
|
"Play Voice": "Play Voice",
|
||||||
|
"Voice Example": "This is an example text for testing speech synthesis",
|
||||||
|
"Synthesizing Voice": "Synthesizing voice, please wait...",
|
||||||
|
"TTS Provider": "Select the voice synthesis provider"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
78
webui/i18n/vi.json
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
{
|
||||||
|
"Language": "Tiếng Việt",
|
||||||
|
"Translation": {
|
||||||
|
"Video Script Settings": "**Cài Đặt Kịch Bản Video**",
|
||||||
|
"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)",
|
||||||
|
"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)",
|
||||||
|
"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ủ đề**",
|
||||||
|
"Auto Detect": "Tự Động Phát Hiện",
|
||||||
|
"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ụ đề)",
|
||||||
|
"Generate Video Keywords": "Nhấn để sử dụng AI để tạo [Từ Khóa Video] dựa trên **kịch bản**",
|
||||||
|
"Please Enter the Video Subject": "Vui lòng Nhập Kịch Bản Video Trước",
|
||||||
|
"Generating Video Script and Keywords": "AI đang tạo kịch bản video và từ khóa...",
|
||||||
|
"Generating Video Keywords": "AI đang tạo từ khóa video...",
|
||||||
|
"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])",
|
||||||
|
"Video Settings": "**Cài Đặt Video**",
|
||||||
|
"Video Concat Mode": "Chế Độ Nối Video",
|
||||||
|
"Random": "Nối Ngẫu Nhiên (Được Khuyến Nghị)",
|
||||||
|
"Sequential": "Nối Theo Thứ Tự",
|
||||||
|
"Video Ratio": "Tỷ Lệ Khung Hình Video",
|
||||||
|
"Portrait": "Dọc 9:16",
|
||||||
|
"Landscape": "Ngang 16:9",
|
||||||
|
"Clip Duration": "Thời Lượng Tối Đa Của Đoạn Video (giây)",
|
||||||
|
"Number of Videos Generated Simultaneously": "Số Video Được Tạo Ra Đồng Thời",
|
||||||
|
"Audio Settings": "**Cài Đặt Âm Thanh**",
|
||||||
|
"Speech Synthesis": "Giọng Đọc Văn Bản",
|
||||||
|
"Speech Region": "Vùng(:red[Bắt Buộc,[Lấy Vùng](https://portal.azure.com/#view/Microsoft_Azure_ProjectOxford/CognitiveServicesHub/~/SpeechServices)])",
|
||||||
|
"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)])",
|
||||||
|
"Speech Volume": "Âm Lượng Giọng Đọc (1.0 đại diện cho 100%)",
|
||||||
|
"Male": "Nam",
|
||||||
|
"Female": "Nữ",
|
||||||
|
"Background Music": "Âm Nhạc Nền",
|
||||||
|
"No Background Music": "Không Có Âm Nhạc Nền",
|
||||||
|
"Random Background Music": "Âm Nhạc Nền Ngẫu Nhiên",
|
||||||
|
"Custom Background Music": "Âm Nhạc Nền Tùy Chỉnh",
|
||||||
|
"Custom Background Music File": "Vui lòng nhập đường dẫn tệp cho âm nhạc nền tùy chỉnh:",
|
||||||
|
"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)",
|
||||||
|
"Subtitle Settings": "**Cài Đặt Phụ Đề**",
|
||||||
|
"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)",
|
||||||
|
"Font": "Phông Chữ Phụ Đề",
|
||||||
|
"Position": "Vị Trí Phụ Đề",
|
||||||
|
"Top": "Trên",
|
||||||
|
"Center": "Giữa",
|
||||||
|
"Bottom": "Dưới (Được Khuyến Nghị)",
|
||||||
|
"Font Size": "Cỡ Chữ Phụ Đề",
|
||||||
|
"Font Color": "Màu Chữ Phụ Đề",
|
||||||
|
"Stroke Color": "Màu Viền Phụ Đề",
|
||||||
|
"Stroke Width": "Độ Rộng Viền Phụ Đề",
|
||||||
|
"Generate Video": "Tạo Video",
|
||||||
|
"Video Script and Subject Cannot Both Be Empty": "Chủ Đề Video và Kịch Bản Video không thể cùng trống",
|
||||||
|
"Generating Video": "Đang tạo video, vui lòng đợi...",
|
||||||
|
"Start Generating Video": "Bắt Đầu Tạo Video",
|
||||||
|
"Video Generation Completed": "Hoàn Tất Tạo Video",
|
||||||
|
"Video Generation Failed": "Tạo Video Thất Bại",
|
||||||
|
"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",
|
||||||
|
"Pexels API Key": "Khóa API Pexels ([Lấy Khóa API](https://www.pexels.com/api/))",
|
||||||
|
"Pixabay API Key": "Pixabay API Key ([Get API Key](https://pixabay.com/api/docs/#api_search_videos))",
|
||||||
|
"Basic Settings": "**Cài Đặt Cơ Bản** (:blue[Nhấp để mở rộng])",
|
||||||
|
"Language": "Ngôn Ngữ",
|
||||||
|
"LLM Provider": "Nhà Cung Cấp LLM",
|
||||||
|
"API Key": "Khóa API (:red[Bắt Buộc])",
|
||||||
|
"Base Url": "Url Cơ Bản",
|
||||||
|
"Account ID": "ID Tài Khoản (Lấy từ bảng điều khiển Cloudflare)",
|
||||||
|
"Model Name": "Tên Mô Hình",
|
||||||
|
"Please Enter the LLM API Key": "Vui lòng Nhập **Khóa API LLM**",
|
||||||
|
"Please Enter the Pexels API Key": "Vui lòng Nhập **Khóa API Pexels**",
|
||||||
|
"Please Enter the Pixabay API Key": "Vui lòng Nhập **Pixabay API Key**",
|
||||||
|
"Get Help": "Nếu bạn cần giúp đỡ hoặc có bất kỳ câu hỏi nào, bạn có thể tham gia discord để được giúp đỡ: https://harryai.cc",
|
||||||
|
"Video Source": "Video Source",
|
||||||
|
"TikTok": "TikTok (TikTok support is coming soon)",
|
||||||
|
"Bilibili": "Bilibili (Bilibili support is coming soon)",
|
||||||
|
"Xiaohongshu": "Xiaohongshu (Xiaohongshu support is coming soon)",
|
||||||
|
"Local file": "Local file",
|
||||||
|
"Play Voice": "Play Voice",
|
||||||
|
"Voice Example": "This is an example text for testing speech synthesis",
|
||||||
|
"Synthesizing Voice": "Synthesizing voice, please wait...",
|
||||||
|
"TTS Provider": "Select the voice synthesis provider"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -19,10 +19,12 @@
|
|||||||
"Video Ratio": "视频比例",
|
"Video Ratio": "视频比例",
|
||||||
"Portrait": "竖屏 9:16(抖音视频)",
|
"Portrait": "竖屏 9:16(抖音视频)",
|
||||||
"Landscape": "横屏 16:9(西瓜视频)",
|
"Landscape": "横屏 16:9(西瓜视频)",
|
||||||
"Clip Duration": "视频片段最大时长(秒)",
|
"Clip Duration": "视频片段最大时长(秒)(**不是视频总长度**,是指每个**合成片段**的长度)",
|
||||||
"Number of Videos Generated Simultaneously": "同时生成视频数量",
|
"Number of Videos Generated Simultaneously": "同时生成视频数量",
|
||||||
"Audio Settings": "**音频设置**",
|
"Audio Settings": "**音频设置**",
|
||||||
"Speech Synthesis": "朗读声音(:red[尽量与文案语言保持一致])",
|
"Speech Synthesis": "朗读声音(:red[**与文案语言保持一致**。注意:V2版效果更好,但是需要API KEY])",
|
||||||
|
"Speech Region": "服务区域 (:red[必填,[点击获取](https://portal.azure.com/#view/Microsoft_Azure_ProjectOxford/CognitiveServicesHub/~/SpeechServices)])",
|
||||||
|
"Speech Key": "API Key (:red[必填,密钥1 或 密钥2 均可 [点击获取](https://portal.azure.com/#view/Microsoft_Azure_ProjectOxford/CognitiveServicesHub/~/SpeechServices)])",
|
||||||
"Speech Volume": "朗读音量(1.0表示100%)",
|
"Speech Volume": "朗读音量(1.0表示100%)",
|
||||||
"Male": "男性",
|
"Male": "男性",
|
||||||
"Female": "女性",
|
"Female": "女性",
|
||||||
@@ -48,16 +50,29 @@
|
|||||||
"Generating Video": "正在生成视频,请稍候...",
|
"Generating Video": "正在生成视频,请稍候...",
|
||||||
"Start Generating Video": "开始生成视频",
|
"Start Generating Video": "开始生成视频",
|
||||||
"Video Generation Completed": "视频生成完成",
|
"Video Generation Completed": "视频生成完成",
|
||||||
|
"Video Generation Failed": "视频生成失败",
|
||||||
"You can download the generated video from the following links": "你可以从以下链接下载生成的视频",
|
"You can download the generated video from the following links": "你可以从以下链接下载生成的视频",
|
||||||
"Basic Settings": "**基础设置** (:blue[点击展开])",
|
"Basic Settings": "**基础设置** (:blue[点击展开])",
|
||||||
"Language": "界面语言",
|
"Language": "界面语言",
|
||||||
"Pexels API Key": "Pexels API Key (:red[必填] [点击获取](https://www.pexels.com/api/))",
|
"Pexels API Key": "Pexels API Key ([点击获取](https://www.pexels.com/api/))",
|
||||||
|
"Pixabay API Key": "Pixabay API Key ([点击获取](https://pixabay.com/api/docs/#api_search_videos))",
|
||||||
"LLM Provider": "大模型提供商",
|
"LLM Provider": "大模型提供商",
|
||||||
"API Key": "API Key (:red[必填,需要到大模型提供商的后台申请])",
|
"API Key": "API Key (:red[必填,需要到大模型提供商的后台申请])",
|
||||||
"Base Url": "Base Url (可选)",
|
"Base Url": "Base Url (可选)",
|
||||||
|
"Account ID": "账户ID (Cloudflare的dash面板url中获取)",
|
||||||
"Model Name": "模型名称 (:blue[需要到大模型提供商的后台确认被授权的模型名称])",
|
"Model Name": "模型名称 (:blue[需要到大模型提供商的后台确认被授权的模型名称])",
|
||||||
"Please Enter the LLM API Key": "请先填写大模型 **API Key**",
|
"Please Enter the LLM API Key": "请先填写大模型 **API Key**",
|
||||||
"Please Enter the Pexels API Key": "请先填写 **Pexels API Key**",
|
"Please Enter the Pexels API Key": "请先填写 **Pexels API Key**",
|
||||||
"Get Help": "有任何问题或建议,可以加入 **微信群** 求助或讨论:https://harryai.cc"
|
"Please Enter the Pixabay API Key": "请先填写 **Pixabay API Key**",
|
||||||
|
"Get Help": "有任何问题或建议,可以加入 **微信群** 求助或讨论:https://harryai.cc",
|
||||||
|
"Video Source": "视频来源",
|
||||||
|
"TikTok": "抖音 (TikTok 支持中,敬请期待)",
|
||||||
|
"Bilibili": "哔哩哔哩 (Bilibili 支持中,敬请期待)",
|
||||||
|
"Xiaohongshu": "小红书 (Xiaohongshu 支持中,敬请期待)",
|
||||||
|
"Local file": "本地文件",
|
||||||
|
"Play Voice": "试听语音合成",
|
||||||
|
"Voice Example": "这是一段测试语音合成的示例文本",
|
||||||
|
"Synthesizing Voice": "语音合成中,请稍候...",
|
||||||
|
"TTS Provider": "语音合成提供商"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||