165 Commits

Author SHA1 Message Date
Harry
09ad53a60e Merge pull request #369 from harry0703/dev
update version to 1.1.9
2024-05-16 16:36:54 +08:00
harry
bb4333db95 update version to 1.1.9 2024-05-16 16:36:26 +08:00
Harry
dc460c25d2 Merge pull request #368 from harry0703/dev
enhanced exception handling for llm and optimized video concatenation
2024-05-16 16:35:56 +08:00
harry
4ca8d8d8ae enhanced exception handling for llm and optimized video concatenation 2024-05-16 16:34:31 +08:00
harry
37e56239d9 remove log 2024-05-16 16:23:58 +08:00
Harry
ba8613baeb Merge pull request #366 from ATtendev/ft/shuffle
update shuffle video
2024-05-16 15:10:09 +08:00
Harry
fd8ed3bd69 Merge pull request #367 from harry0703/dev
update readme
2024-05-16 15:07:59 +08:00
harry
1b8b65f642 update readme 2024-05-16 15:07:31 +08:00
AT
2128a5fae5 chore: true shuffle video
- random duration splits video
- shuffle splits video
2024-05-16 10:11:10 +07:00
Harry
2976652509 Merge pull request #365 from harry0703/dev
update version to 1.1.8
2024-05-15 22:42:15 +08:00
harry
eb2b07b615 update version to 1.1.8 2024-05-15 22:41:52 +08:00
Harry
289c06799a Merge pull request #364 from harry0703/dev
support pixabay
2024-05-15 22:40:22 +08:00
harry
afb4eff3e5 support pixabay 2024-05-15 22:39:01 +08:00
harry
bdf7af0a12 fix the bug with the incorrect bgm_type parameter 2024-05-15 11:33:16 +08:00
Harry
ee931d1933 Merge pull request #362 from harry0703/dev
update readme
2024-05-14 13:48:43 +08:00
harry
cbd3495426 update readme 2024-05-14 13:48:18 +08:00
Harry
a864be83da Merge pull request #357 from harry0703/dev
support deepseek llm
2024-05-13 21:49:21 +08:00
harry
fee226a149 support deepseek llm 2024-05-13 21:48:53 +08:00
Harry
b3f549b4db Merge pull request #356 from harry0703/dev
update readme
2024-05-13 18:37:29 +08:00
harry
4c21a23a8b update readme 2024-05-13 18:36:58 +08:00
Harry
8d84b5b530 Merge pull request #355 from harry0703/dev
support voice preview, update version to 1.1.6
2024-05-13 18:34:13 +08:00
harry
6de3d6eedc 1. support voice preview
2, update version to 1.1.6
2024-05-13 18:29:59 +08:00
Harry
ee680d24cc Merge pull request #348 from harry0703/dev
update example configuration
2024-05-10 14:49:53 +08:00
harry
fb3aadeccc update example configuration 2024-05-10 14:49:18 +08:00
Harry
5254577e5c Merge pull request #346 from harry0703/dev
update wechat qrcode
2024-05-10 08:46:23 +08:00
harry
c15a3bdd13 update wechat qrcode 2024-05-10 08:46:01 +08:00
Harry
d8d8a5b602 Merge pull request #340 from harry0703/dev
update readme
2024-05-07 09:59:48 +08:00
harry
e258617d4f update readme 2024-05-07 09:58:07 +08:00
Harry
ce64602c08 Merge pull request #338 from harry0703/dev
update readme
2024-05-05 18:09:02 +08:00
harry
f729124a48 update readme 2024-05-05 18:08:32 +08:00
Harry
367018b3f7 Merge pull request #337 from harry0703/dev
update readme
2024-05-05 18:07:52 +08:00
harry
b055755689 update readme 2024-05-05 18:07:30 +08:00
Harry
c8894a851b Merge pull request #332 from harry0703/dev
update readme
2024-04-28 22:52:40 +08:00
harry
28c5bc372f update readme 2024-04-28 22:50:49 +08:00
Harry
7ed2603442 Merge pull request #331 from harry0703/dev
update readme
2024-04-28 17:20:32 +08:00
harry
1f8b41d2b3 update readme 2024-04-28 17:19:59 +08:00
Harry
ee09cf64d5 Merge pull request #330 from harry0703/dev
update readme
2024-04-28 17:17:16 +08:00
harry
4e886a1a73 update readme 2024-04-28 17:15:48 +08:00
Harry
661d8cb5ab Merge pull request #325 from harry0703/dev
support local videos
2024-04-27 08:52:10 +08:00
harry
4de02f4429 update version and readme 2024-04-27 08:35:40 +08:00
harry
5d06530a39 support local videos 2024-04-27 08:33:44 +08:00
Harry
4596804bcf Merge pull request #321 from harry0703/dev
update tips and add hide_config
2024-04-26 14:49:20 +08:00
harry
c161ab3124 add hide_config 2024-04-26 14:48:33 +08:00
harry
376955d4a0 update tips 2024-04-26 14:43:11 +08:00
Harry
b08b79f9cf Merge pull request #320 from harry0703/dev
1. Added exception handling when loading the Whisper model. 2. Check if the file exists before verifying the subtitles.
2024-04-26 11:15:17 +08:00
harry
7582022fa9 1. Added exception handling when loading the Whisper model.
2. Check if the file exists before verifying the subtitles.
2024-04-26 11:14:35 +08:00
Harry
8dbce05344 Merge pull request #319 from harry0703/dev
update readme
2024-04-26 11:09:09 +08:00
harry
4d91a83858 update readme 2024-04-26 11:08:44 +08:00
Harry
8e93dd7ca0 Merge pull request #318 from KPCOFGS/main
更新了英文的README文件
2024-04-26 10:28:30 +08:00
Shi Sheng
d2277715df Update README-en.md 2024-04-25 15:06:29 -04:00
Shi Sheng
8861526e0a Update README-en.md 2024-04-25 15:03:51 -04:00
Shi Sheng
f38fc60394 Update README-en.md 2024-04-25 15:01:56 -04:00
Shi Sheng
e77bff3ffb Update README-en.md 2024-04-25 15:00:53 -04:00
Shi Sheng
59a518ce9d Update README-en.md 2024-04-25 14:59:39 -04:00
Harry
d922ff2576 Merge pull request #315 from harry0703/dev
update readme
2024-04-25 10:50:53 +08:00
harry
84b3ef13c0 update readme 2024-04-25 10:49:28 +08:00
Harry
a53da162ac Merge pull request #311 from harry0703/dev
RUN chmod 777 /MoneyPrinterTurbo
2024-04-24 13:48:08 +08:00
harry
e77389ffb5 RUN chmod 777 /MoneyPrinterTurbo 2024-04-24 13:47:37 +08:00
Harry
2be09365d9 Merge pull request #310 from harry0703/dev
update readme
2024-04-24 13:46:25 +08:00
harry
f85c11118d update readme 2024-04-24 13:45:57 +08:00
Kevin Zhang
419abd760e Merge pull request #290 from KevinZhang19870314/main
doc: landing for MoneyPrinterTurbo
2024-04-23 10:58:07 +08:00
Harry
5c0a905c09 Merge pull request #302 from harry0703/dev
update docker base image
2024-04-22 18:33:34 +08:00
harry
3c5ef29775 update docker base image 2024-04-22 18:33:00 +08:00
Harry
bc45d4bcf3 Merge pull request #301 from harry0703/dev
optimize UI and code
2024-04-22 17:59:40 +08:00
harry
e38c79bfad rollback docker base image 2024-04-22 17:59:04 +08:00
harry
c7c7b4847e optimize code 2024-04-22 16:25:13 +08:00
harry
6a5c4e9e73 optimize UI 2024-04-22 16:24:32 +08:00
harry
2eb6d4b5cc update example configuration and library 2024-04-22 16:00:33 +08:00
Harry
13f3abffd0 Merge pull request #294 from harry0703/dev
fixed: WS_OPEN_ERROR_UNDERLYING_IO_OPEN_FAILED
2024-04-22 08:41:10 +08:00
harry
6164920eaa fixed: WS_OPEN_ERROR_UNDERLYING_IO_OPEN_FAILED, thanks to https://github.com/oicid 2024-04-22 08:40:32 +08:00
kevin.zhang
a8d528f41c Merge branch 'main' of https://github.com/KevinZhang19870314/MoneyPrinterTurbo 2024-04-19 14:23:59 +08:00
kevin.zhang
259b3e94fc doc: sites 2024-04-19 14:22:45 +08:00
Kevin Zhang
ffcb52fd46 Merge pull request #286 from KevinZhang19870314/dev 2024-04-18 18:18:32 +08:00
kevin.zhang
ee4337e847 chore: test sites changes 2024-04-18 18:08:47 +08:00
kevin.zhang
16bcec96ee chore: fix 2024-04-18 18:07:53 +08:00
kevin.zhang
cc231e0b62 chore: fix 2024-04-18 18:07:10 +08:00
kevin.zhang
cb2c53334e chore: fix 2024-04-18 17:58:58 +08:00
kevin.zhang
643b0fb30c chore: fix 2024-04-18 17:43:29 +08:00
kevin.zhang
6b97e7dbd4 chore: fix 2024-04-18 17:38:13 +08:00
kevin.zhang
4804aa1e04 chore: ci/cd 2024-04-18 17:35:37 +08:00
kevin.zhang
c04e3988fe Merge branch 'main' of https://github.com/KevinZhang19870314/MoneyPrinterTurbo 2024-04-18 16:15:29 +08:00
kevin.zhang
c9eb16e0a9 chore: fix 2024-04-18 15:44:57 +08:00
kevin.zhang
2d599db892 feat: add sites for MoneyPrinterTurbo 2024-04-18 15:23:02 +08:00
Harry
ec50cd0184 Merge pull request #285 from vuisme/main
Update requirements.txt
2024-04-18 15:10:22 +08:00
cpanel10x
a1a1a51881 Update requirements.txt
Fix error
streamlit 1.32.0 depends on packaging<24 
git-changelog 2.5.2 depends on packaging>=24.0
2024-04-18 14:01:12 +07:00
Harry
0066bab3ec Merge pull request #283 from KevinZhang19870314/dev
chore: add changelog auto gen
2024-04-18 10:41:40 +08:00
kevin.zhang
2f461d961c Merge branch 'dev' of https://github.com/KevinZhang19870314/MoneyPrinterTurbo into dev 2024-04-18 10:29:22 +08:00
kevin.zhang
77c250ce18 chore: add gitattributes 2024-04-18 10:29:05 +08:00
kevin.zhang
3f941c8dd9 chore: add changelog auto gen 2024-04-18 09:48:58 +08:00
Harry
ab5ae7072b Merge pull request #280 from harry0703/dev
v1.1.3
2024-04-17 17:26:47 +08:00
harry
93da539519 update version to 1.1.3 2024-04-17 17:25:28 +08:00
harry
5280159f41 added microsoft yahei fonts 2024-04-17 17:25:10 +08:00
harry
729c407c30 fixed: ValidationError: 1 validation error for VideoParams video_subject Field required 2024-04-17 17:24:00 +08:00
harry
73ec0cf7ad Merge remote-tracking branch 'origin/dev' into dev 2024-04-17 17:17:55 +08:00
harry
a5273b31b3 Merge branch 'main' into dev 2024-04-17 17:13:41 +08:00
Harry
f83c374ef4 Merge pull request #277 from vuisme/main
Add Vietnamese
2024-04-17 17:12:18 +08:00
Harry
da010c476f Merge pull request #278 from KevinZhang19870314/dev
chore: optimize Dockerfile
2024-04-17 17:10:50 +08:00
kevin.zhang
78f5ce7cdd Merge branch 'dev' of https://github.com/KevinZhang19870314/MoneyPrinterTurbo into dev 2024-04-17 16:02:06 +08:00
kevin.zhang
27553114b0 chore: optimize Dockerfile 2024-04-17 16:01:45 +08:00
vuisme
4ae022c059 clean 2024-04-17 11:07:20 +07:00
vuisme
70ed2b5c82 clean 2024-04-17 11:07:07 +07:00
vuisme
89f001742a rm 2024-04-17 11:04:45 +07:00
cpanel10x
6c8b4f665d Merge branch 'harry0703:main' into main 2024-04-17 10:59:22 +07:00
vuisme
1c35e50563 Add vietnamese and sample font Vietnamese. String pre-translated by chatGPT 2024-04-17 10:57:16 +07:00
Harry
add34a92f7 Merge pull request #267 from KevinZhang19870314/dev
feat: add support for maximum concurrency of /api/v1/videos
2024-04-17 11:09:03 +08:00
Harry
a44016a7cf Merge pull request #275 from harry0703/dev
add timeout
2024-04-17 10:56:18 +08:00
harry
0521d46826 add timeout 2024-04-17 10:55:57 +08:00
cpanel10x
f48fb24dcf Merge branch 'harry0703:main' into main 2024-04-17 09:47:16 +07:00
Harry
996cd55462 Merge pull request #272 from harry0703/dev
update readme
2024-04-16 22:15:23 +08:00
harry
97640d2199 update readme 2024-04-16 22:14:51 +08:00
Kevin Zhang
f6857d63f9 Merge branch 'harry0703:dev' into dev 2024-04-16 17:51:46 +08:00
kevin.zhang
abe12abd7b feat: add support for maximum concurrency of /api/v1/videos 2024-04-16 17:47:56 +08:00
cpanel10x
7e8c901fd4 Merge branch 'harry0703:main' into main 2024-04-16 15:28:30 +07:00
Harry
1cee2bbb8d Merge pull request #266 from harry0703/dev
update readme
2024-04-16 14:25:57 +08:00
harry
4d176c6107 update readme 2024-04-16 14:25:35 +08:00
Harry
e499f8a1c4 Merge pull request #265 from harry0703/dev
update readme
2024-04-16 09:48:28 +08:00
harry
d82b5f76e0 update readme 2024-04-16 09:47:01 +08:00
Harry
414bcb0621 Merge pull request #264 from harry0703/dev
support azure new speech voice and fix the bug where clip were not closed
2024-04-16 09:00:10 +08:00
harry
d4eb7bc333 optimize code 2024-04-15 17:47:10 +08:00
harry
1e96357f00 fix the bug where the last subtitle line was missing 2024-04-15 17:46:56 +08:00
harry
2e58d7ccf2 fix the bug where clip were not closed 2024-04-15 17:46:24 +08:00
harry
176660b442 support azure new speech voice 2024-04-15 17:45:05 +08:00
Harry
b9b9bea2a6 Merge pull request #261 from KevinZhang19870314/main
chore: add video download api endpoint
2024-04-15 17:23:29 +08:00
Kevin Zhang
17df9a1f27 Merge branch 'harry0703:main' into main 2024-04-15 14:49:36 +08:00
kevin.zhang
00052b4c50 chore: add video download api endpoint 2024-04-15 14:47:57 +08:00
Harry
b8369349ea Merge pull request #260 from harry0703/dev
optimize subtitle segmentation and code
2024-04-15 11:30:33 +08:00
Harry
3de3e19276 Merge pull request #259 from KevinZhang19870314/main
refactor: video stream api revise
2024-04-15 11:30:10 +08:00
harry
bd33419460 optimize subtitle segmentation
optimize code
2024-04-15 11:29:04 +08:00
kevin.zhang
d13a3cf6e9 refactor: Streaming MP4 files in the browser using video html element instead of waiting for the entire file to download before playing 2024-04-15 09:51:40 +08:00
Harry
3e4d5f52fd Merge pull request #258 from harry0703/dev
update readme
2024-04-14 21:18:11 +08:00
harry
9a1ee9abfb update readme 2024-04-14 21:16:51 +08:00
Harry
2c41e6be62 Merge pull request #254 from harry0703/dev
optimize segmentation
2024-04-13 21:51:33 +08:00
harry
a17d52c1ae optimize segmentation 2024-04-13 21:50:45 +08:00
Harry
b1506b9161 Merge pull request #253 from harry0703/dev
optimize segmentation
2024-04-13 21:04:22 +08:00
harry
53923e0d25 optimize segmentation 2024-04-13 21:03:55 +08:00
Harry
1a302a1791 Merge pull request #252 from harry0703/dev
fix some bugs
2024-04-13 20:26:52 +08:00
harry
ce0f557702 1. prioritize using the bgm_file.
2. optimized the logic for looping the BGM.
2024-04-13 20:24:09 +08:00
harry
a8d208bdc3 added validation for video file using moviepy to ensure video file is valid before processing. 2024-04-13 20:19:08 +08:00
Harry
0cb71d6218 Merge pull request #251 from harry0703/dev
write_videofile set fps=30
2024-04-12 22:02:25 +08:00
harry
52b92d175d write_videofile set fps=30 2024-04-12 22:02:01 +08:00
Harry
76e1407d9b Merge pull request #250 from harry0703/dev
update readme
2024-04-12 18:54:51 +08:00
harry
26437a666c update readme 2024-04-12 18:54:25 +08:00
Harry
8907958fec Merge pull request #249 from KevinZhang19870314/main
add password support for redis state and stream api support for video
2024-04-12 17:54:35 +08:00
kevin.zhang
0550e433d1 Merge branch 'main' of https://github.com/KevinZhang19870314/MoneyPrinterTurbo 2024-04-12 17:47:14 +08:00
kevin.zhang
1fb3399b02 chore: add stream support for video 2024-04-12 17:43:21 +08:00
Harry
9ab13a74a2 Merge pull request #248 from elf-mouse/main
fix: response parsing bug for gemini
2024-04-12 17:03:01 +08:00
elf-mouse
1dbfcfadab Merge branch 'main' of github.com:elf-mouse/MoneyPrinterTurbo 2024-04-12 16:50:32 +08:00
elf-mouse
ee7306d216 fix: response parsing bug for gemini 2024-04-12 15:49:23 +08:00
Harry
a8b54415a5 Merge pull request #242 from harry0703/dev
update readme
2024-04-12 10:22:40 +08:00
harry
7a8e25dc36 update readme 2024-04-12 10:22:01 +08:00
Harry
c8adc453ae Merge pull request #241 from harry0703/dev
add qwen error logs
2024-04-12 10:08:08 +08:00
harry
24a9ca514e add qwen error logs 2024-04-12 10:05:14 +08:00
harry
a7466b2393 add qwen error logs 2024-04-12 10:04:52 +08:00
Harry
1f2b36a4f0 Merge pull request #240 from harry0703/dev
fix webui.bat and docker-compose.yml
2024-04-11 23:40:21 +08:00
harry
91218ecf95 fix webui.bat and docker-compose.yml 2024-04-11 23:39:49 +08:00
Harry
a0a5a4059f Merge pull request #238 from highkay/main
增加Cloudflare workers ai作为llm后端
2024-04-11 23:11:17 +08:00
highkay
90f0f560b2 Merge branch 'main' of https://github.com/harry0703/MoneyPrinterTurbo into main 2024-04-11 22:55:18 +08:00
highkay
05da4a3766 - 增加Cloudflare workers ai作为llm后端
- 增加一些gitignore
2024-04-11 22:55:08 +08:00
Harry
066e33def9 Merge pull request #237 from harry0703/dev
update readme
2024-04-11 22:03:17 +08:00
harry
bb66b7e10c update readme 2024-04-11 22:02:00 +08:00
cpanel10x
1ac643f4d0 Merge branch 'harry0703:main' into main 2024-04-10 16:01:43 +07:00
cpanel10x
fea396585f Merge branch 'harry0703:main' into main 2024-04-10 14:19:13 +07:00
cpanel10x
3fe6ff42c8 Merge branch 'harry0703:main' into main 2024-04-09 16:04:36 +07:00
cpanel10x
a71e54fd7c Merge branch 'harry0703:main' into main 2024-03-28 10:22:36 +07:00
cpanel10x
fee660fb8c Added Dev Container Folder 2024-03-27 15:27:34 +07:00
94 changed files with 5978 additions and 394 deletions

View File

@@ -21,3 +21,4 @@ __pycache__/
.svn/ .svn/
storage/ storage/
config.toml

33
.github/workflows/gh-pages.yml vendored Normal file
View 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
View File

@@ -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
View 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).

View File

@@ -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

View File

@@ -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.
@@ -61,7 +61,6 @@ https://reccloud.com
❓[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 📅
- [ ] Introduce support for GPT-SoVITS dubbing - [ ] Introduce support for GPT-SoVITS dubbing
@@ -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`

200
README.md
View File

@@ -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 @@
![](docs/reccloud.cn.jpg) ![](docs/reccloud.cn.jpg)
## 感谢赞助 🙏
感谢佐糖 https://picwish.cn 对该项目的支持和赞助,使得该项目能够持续的更新和维护。
佐糖专注于**图像处理领域**,提供丰富的**图像处理工具**,将复杂操作极致简化,真正实现让图像处理更简单。
![picwish.jpg](docs/picwish.jpg)
## 功能特性 🎯 ## 功能特性 🎯
- [x] 完整的 **MVC架构**,代码 **结构清晰**,易于维护,支持 `API``Web界面` - [x] 完整的 **MVC架构**,代码 **结构清晰**,易于维护,支持 `API``Web界面`
@@ -44,13 +54,13 @@
- [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 @@
- [ ] 增加视频转场效果,使其看起来更加的流畅 - [ ] 增加视频转场效果,使其看起来更加的流畅
- [ ] 增加更多视频素材来源,优化视频素材和文案的匹配度 - [ ] 增加更多视频素材来源,优化视频素材和文案的匹配度
- [ ] 增加视频长度选项:短、中、长 - [ ] 增加视频长度选项:短、中、长
- [ ] 打包成一键启动包WindowsmacOS方便使用 - [ ] 支持更多的语音合成服务商,比如 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,26 +199,21 @@ pip install -r requirements.txt
#### ② 安装好 ImageMagick #### ② 安装好 ImageMagick
###### Windows: - Windows:
- 下载 https://imagemagick.org/script/download.php 选择Windows版本切记一定要选择 **静态库** 版本,比如
- 下载 https://imagemagick.org/archive/binaries/ImageMagick-7.1.1-30-Q16-x64-static.exe ImageMagick-7.1.1-32-Q16-x64-**static**.exe
- 安装下载好的 ImageMagick注意不要修改安装路径 - 安装下载好的 ImageMagick**注意不要修改安装路径**
- 修改 `配置文件 config.toml` 中的 `imagemagick_path` 为你的实际安装路径(如果安装的时候没有修改路径,直接取消注释即可) - 修改 `配置文件 config.toml` 中的 `imagemagick_path` 为你的 **实际安装路径**
###### MacOS:
- MacOS:
```shell ```shell
brew install imagemagick brew install imagemagick
```` ````
- Ubuntu
###### Ubuntu
```shell ```shell
sudo apt-get install imagemagick sudo apt-get install imagemagick
``` ```
- CentOS
###### CentOS
```shell ```shell
sudo yum install ImageMagick sudo yum install ImageMagick
``` ```
@@ -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
[![Star History Chart](https://api.star-history.com/svg?repos=harry0703/MoneyPrinterTurbo&type=Date)](https://star-history.com/#harry0703/MoneyPrinterTurbo&Date) [![Star History Chart](https://api.star-history.com/svg?repos=harry0703/MoneyPrinterTurbo&type=Date)](https://star-history.com/#harry0703/MoneyPrinterTurbo&Date)

View File

@@ -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}")

View 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()

View 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()

View 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

View File

@@ -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:]}')

View File

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

View File

@@ -1,6 +1,7 @@
from enum import Enum from enum import Enum
from typing import Any, Optional from typing import Any, Optional, List
import pydantic
from pydantic import BaseModel from pydantic import BaseModel
import warnings import warnings
@@ -28,6 +29,11 @@ class VideoAspect(str, Enum):
return 1080, 1920 return 1080, 1920
class _Config:
arbitrary_types_allowed = True
@pydantic.dataclasses.dataclass(config=_Config)
class MaterialInfo: class MaterialInfo:
provider: str = "pexels" provider: str = "pexels"
url: str = "" url: str = ""
@@ -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] = ""

View File

@@ -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)

View File

@@ -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")

View File

@@ -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()

View File

@@ -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 = ""

View File

@@ -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")

View File

@@ -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
# )

View File

@@ -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")

View File

@@ -67,10 +67,13 @@ def root_dir():
return os.path.dirname(os.path.dirname(os.path.dirname(os.path.realpath(__file__)))) return os.path.dirname(os.path.dirname(os.path.dirname(os.path.realpath(__file__))))
def storage_dir(sub_dir: str = ""): def storage_dir(sub_dir: str = "", create: bool = False):
d = os.path.join(root_dir(), "storage") d = os.path.join(root_dir(), "storage")
if sub_dir: if sub_dir:
d = os.path.join(d, sub_dir) d = os.path.join(d, sub_dir)
if create and not os.path.exists(d):
os.makedirs(d)
return d return d
@@ -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
View 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,
)

View File

@@ -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=""

View File

@@ -1,5 +1,3 @@
version: "3"
x-common-volumes: &common-volumes x-common-volumes: &common-volumes
- ./:/MoneyPrinterTurbo - ./:/MoneyPrinterTurbo

BIN
docs/picwish.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 178 KiB

BIN
docs/wechat-group.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 190 KiB

View File

@@ -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

Binary file not shown.

Binary file not shown.

Binary file not shown.

View 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"],
},
];
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 252 KiB

View File

Before

Width:  |  Height:  |  Size: 172 KiB

After

Width:  |  Height:  |  Size: 172 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 MiB

View 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"
}
]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 178 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 294 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 256 KiB

View File

Before

Width:  |  Height:  |  Size: 100 KiB

After

Width:  |  Height:  |  Size: 100 KiB

View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 340 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 384 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 340 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 166 KiB

16
sites/docs/README.md Normal file
View 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
View 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
![](/webui-en.jpg)
### API Interface
![](/api.jpg)
- 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
[![Star History Chart](https://api.star-history.com/svg?repos=harry0703/MoneyPrinterTurbo&type=Date)](https://star-history.com/#harry0703/MoneyPrinterTurbo&Date)

View 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
View 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

View 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

View 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).

View 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.

View File

@@ -0,0 +1,3 @@
## Voice Synthesis 🗣
A list of all supported voices can be viewed here: [Voice List](/voice-list.txt)

View 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.

View 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.

View 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
View 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
---

View File

@@ -0,0 +1,157 @@
## 快速开始 🚀
<br>
只需提供一个视频 <b>主题</b> 或 <b>关键词</b> ,就可以全自动生成视频文案、视频素材、视频字幕、视频背景音乐,然后合成一个高清的短视频。
<br>
<h4>Web界面</h4>
![](/webui.jpg)
<h4>API界面</h4>
![](/api.jpg)
下载一键启动包,解压直接使用
### 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
[![Star History Chart](https://api.star-history.com/svg?repos=harry0703/MoneyPrinterTurbo&type=Date)](https://star-history.com/#harry0703/MoneyPrinterTurbo&Date)

View File

@@ -0,0 +1,4 @@
## 背景音乐 🎵
用于视频的背景音乐,位于项目的 `resource/songs` 目录下。
> 当前项目里面放了一些默认的音乐,来自于 YouTube 视频,如有侵权,请删除。

View File

@@ -0,0 +1,4 @@
## 配置要求 📦
- 建议最低 CPU 4核或以上内存 8G 或以上,显卡非必须
- Windows 10 或 MacOS 11.0 以上系统

123
sites/docs/zh/guide/faq.md Normal file
View 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
```

View 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平台

View File

@@ -0,0 +1,4 @@
## 反馈建议 📢
- 可以提交 [issue](https://github.com/harry0703/MoneyPrinterTurbo/issues)
或者 [pull request](https://github.com/harry0703/MoneyPrinterTurbo/pulls)。

View File

@@ -0,0 +1,4 @@
## 参考项目 📚
该项目基于 https://github.com/FujiwaraChoki/MoneyPrinter 重构而来,做了大量的优化,增加了更多的功能。
感谢原作者的开源精神。

View File

@@ -0,0 +1,9 @@
## 特别感谢 🙏
由于该项目的 **部署****使用**,对于一些小白用户来说,还是 **有一定的门槛**,在此特别感谢
**录咖AI智能 多媒体服务平台)** 网站基于该项目,提供的免费`AI视频生成器`服务,可以不用部署,直接在线使用,非常方便。
- 中文版https://reccloud.cn
- 英文版https://reccloud.com
![](/reccloud.cn.jpg)

View File

@@ -0,0 +1,5 @@
## 语音合成 🗣
所有支持的声音列表,可以查看:[声音列表](/voice-list.txt)
2024-04-16 v1.1.2 新增了9种Azure的语音合成声音需要配置API KEY该声音合成的更加真实。

View File

@@ -0,0 +1,3 @@
## 字幕字体 🅰
用于视频字幕的渲染,位于项目的 `resource/fonts` 目录下,你也可以放进去自己的字体。

View 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
```

View File

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

View 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
View 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

File diff suppressed because it is too large Load Diff

View 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
View 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"
}
]
}

View File

@@ -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

View File

@@ -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,6 +151,9 @@ def tr(key):
st.write(tr("Get Help")) st.write(tr("Get Help"))
llm_provider = config.app.get("llm_provider", "").lower()
if not config.app.get("hide_config", False):
with st.expander(tr("Basic Settings"), expanded=False): with st.expander(tr("Basic Settings"), expanded=False):
config_panels = st.columns(3) config_panels = st.columns(3)
left_config_panel = config_panels[0] left_config_panel = config_panels[0]
@@ -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()

View File

@@ -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"
} }
} }

View File

@@ -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
View 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"
}
}

View File

@@ -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": "语音合成提供商"
} }
} }