Skip to content

Commit 06df797

Browse files
author
harry
committed
init
1 parent d4f7b53 commit 06df797

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

71 files changed

+2725
-1
lines changed

.gitignore

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
/config.toml
2+
/storage/
3+
/.idea/

LICENSE

+1-1
Original file line numberDiff line numberDiff line change
@@ -18,4 +18,4 @@ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
1818
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
1919
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
2020
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21-
SOFTWARE.
21+
SOFTWARE.

README.md

+109
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
# MoneyPrinterTurbo 💸
2+
3+
本地自动创建短视频,只需要提供一个视频主题或关键词,就可以全自动生成视频文案、视频素材、视频字幕、视频背景音乐,最后生成一个短视频。
4+
5+
## 效果预览 📺
6+
7+
### 竖屏 9:16
8+
9+
#### 视频演示
10+
11+
[▶️ 竖屏 9:16 Demo1 ](docs/demo-portrait-1.mp4)
12+
13+
[▶️ 竖屏 9:16 Demo2 ](docs/demo-portrait-2.mp4)
14+
15+
#### 图片预览
16+
17+
<img src="docs/demo-portrait-1.jpg" width="300">
18+
<img src="docs/demo-portrait-2.jpg" width="300">
19+
20+
### 横屏 16:9
21+
22+
#### 视频演示
23+
24+
[▶️ 横屏 16:9](docs/demo-landscape.mp4)
25+
26+
#### 图片预览
27+
28+
<img src="docs/demo-landscape.jpg" width="600">
29+
30+
## 安装 📥
31+
32+
建议使用 [conda](https://conda.io/projects/conda/en/latest/user-guide/install/index.html) 创建 python 虚拟环境
33+
34+
```shell
35+
git clone https://github.com/harry0703/MoneyPrinterTurbo.git
36+
cd MoneyPrinterTurbo
37+
conda create -n MoneyPrinterTurbo python=3.10
38+
conda activate MoneyPrinterTurbo
39+
pip install -r requirements.txt
40+
41+
cp config.example.toml config.toml
42+
```
43+
44+
需要先配置 `config.toml` 中的参数
45+
46+
## 使用 🚀
47+
48+
完整的使用演示视频,可以查看:https://v.douyin.com/iFhnwsKY/
49+
50+
请先确认你按照 `config.toml` 文件中的说明,配置好了 `openai_api_key``pexels_api_keys`。否则项目无法正常运行。
51+
52+
### 启动Web界面
53+
54+
```shell
55+
sh webui.sh
56+
```
57+
58+
启动后,会自动打开浏览器,效果如下图:
59+
![](docs/webui.jpg)
60+
61+
### 启动API服务
62+
63+
```shell
64+
python main.py
65+
```
66+
67+
启动后,可以查看 `API文档` http://127.0.0.1:8080/docs
68+
![](docs/api.jpg)
69+
70+
## 语音合成 🗣
71+
72+
所有支持的声音列表,可以查看:[声音列表](./docs/voice-list.txt)
73+
74+
## 字幕生成 📜
75+
76+
当前支持2种字幕生成方式:
77+
78+
- edge
79+
- whisper
80+
81+
可以修改 `config.toml` 配置文件中的 `subtitle_provider` 进行切换,如果留空,表示不生成字幕。
82+
83+
## 背景音乐 🎵
84+
85+
用于视频的背景音乐,位于项目的 `resource/songs` 目录下。当前项目里面放了一些默认的音乐,来自于 YouTube 视频,如有侵权,请删除。
86+
87+
## 字幕字体 🅰
88+
89+
用于视频字幕的渲染,位于项目的 `resource/fonts` 目录下,你也可以放进去自己的字体。
90+
91+
## 反馈和建议 📢
92+
93+
- 可以提交 [issue](https://github.com/harry0703/MoneyPrinterTurbo/issues) 或者 [pull request](https://github.com/harry0703/MoneyPrinterTurbo/pulls)
94+
- 也可以关注我的抖音号:`@网旭哈瑞.AI`
95+
- 我会在上面发布一些 **使用教程****纯技术** 分享。
96+
- 如果有更新和优化,我也会在抖音上面 **及时通知**
97+
- 有问题也可以在抖音上面 **留言**,我会 **尽快回复**
98+
99+
<img src="docs/douyin.jpg" width="500">
100+
101+
## 感谢 🙏
102+
103+
该项目基于 https://github.com/FujiwaraChoki/MoneyPrinter 重构而来,做了大量的优化,增加了更多的功能。
104+
感谢原作者的开源精神。
105+
106+
## License 📝
107+
108+
点击查看 [`LICENSE`](LICENSE) 文件
109+

app/__init__.py

Whitespace-only changes.

app/asgi.py

+60
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
"""Application implementation - ASGI."""
2+
3+
from fastapi import FastAPI, Request
4+
from fastapi.exceptions import RequestValidationError
5+
from fastapi.responses import JSONResponse
6+
from loguru import logger
7+
from fastapi.staticfiles import StaticFiles
8+
9+
from app.config import config
10+
from app.models.exception import HttpException
11+
from app.router import root_api_router
12+
from app.utils import utils
13+
14+
15+
def exception_handler(request: Request, e: HttpException):
16+
return JSONResponse(
17+
status_code=e.status_code,
18+
content=utils.get_response(e.status_code, e.data, e.message),
19+
)
20+
21+
22+
def validation_exception_handler(request: Request, e: RequestValidationError):
23+
return JSONResponse(
24+
status_code=400,
25+
content=utils.get_response(status=400, data=e.errors(), message='field required'),
26+
)
27+
28+
29+
def get_application() -> FastAPI:
30+
"""Initialize FastAPI application.
31+
32+
Returns:
33+
FastAPI: Application object instance.
34+
35+
"""
36+
instance = FastAPI(
37+
title=config.project_name,
38+
description=config.project_description,
39+
version=config.project_version,
40+
debug=False,
41+
)
42+
instance.include_router(root_api_router)
43+
instance.add_exception_handler(HttpException, exception_handler)
44+
instance.add_exception_handler(RequestValidationError, validation_exception_handler)
45+
return instance
46+
47+
48+
app = get_application()
49+
public_dir = utils.public_dir()
50+
app.mount("/", StaticFiles(directory=public_dir, html=True), name="")
51+
52+
53+
@app.on_event("shutdown")
54+
def shutdown_event():
55+
logger.info("shutdown event")
56+
57+
58+
@app.on_event("startup")
59+
def startup_event():
60+
logger.info("startup event")

app/config/__init__.py

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

app/config/config.py

+31
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import os
2+
3+
import tomli
4+
from loguru import logger
5+
6+
root_dir = os.path.dirname(os.path.dirname(os.path.dirname(os.path.realpath(__file__))))
7+
config_file = f"{root_dir}/config.toml"
8+
logger.info(f"load config from file: {config_file}")
9+
10+
with open(config_file, mode="rb") as fp:
11+
_cfg = tomli.load(fp)
12+
13+
app = _cfg.get("app", {})
14+
whisper = _cfg.get("whisper", {})
15+
16+
hostname = os.uname().nodename
17+
18+
log_level = _cfg.get("log_level", "DEBUG")
19+
listen_host = _cfg.get("listen_host", "0.0.0.0")
20+
listen_port = _cfg.get("listen_port", 8080)
21+
project_name = _cfg.get("project_name", "MoneyPrinterTurbo")
22+
project_description = _cfg.get("project_description", "MoneyPrinterTurbo\n by 抖音-网旭哈瑞.AI")
23+
project_version = _cfg.get("project_version", "1.0.0")
24+
reload_debug = False
25+
26+
__cfg = {
27+
"hostname": hostname,
28+
"listen_host": listen_host,
29+
"listen_port": listen_port,
30+
}
31+
logger.info(__cfg)

app/controllers/base.py

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

app/controllers/ping.py

+9
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
from fastapi import APIRouter
2+
from fastapi import Request
3+
4+
router = APIRouter()
5+
6+
7+
@router.get("/ping", tags=["Health Check"], description="检查服务可用性", response_description="pong")
8+
def ping(request: Request) -> str:
9+
return "pong"

app/controllers/v1/base.py

+11
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
from fastapi import APIRouter, Depends
2+
3+
4+
def new_router(dependencies=None):
5+
router = APIRouter()
6+
router.tags = ['V1']
7+
router.prefix = '/api/v1'
8+
# 将认证依赖项应用于所有路由
9+
if dependencies:
10+
router.dependencies = dependencies
11+
return router

app/controllers/v1/video.py

+44
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
from os import path
2+
3+
from fastapi import Request, Depends, Path
4+
from loguru import logger
5+
6+
from app.controllers import base
7+
from app.controllers.v1.base import new_router
8+
from app.models.exception import HttpException
9+
from app.models.schema import TaskVideoRequest, TaskQueryResponse, TaskResponse, TaskQueryRequest
10+
from app.services import task as tm
11+
from app.utils import utils
12+
13+
# 认证依赖项
14+
# router = new_router(dependencies=[Depends(base.verify_token)])
15+
router = new_router()
16+
17+
18+
@router.post("/videos", response_model=TaskResponse, summary="使用主题来生成短视频")
19+
async def create_video(request: Request, body: TaskVideoRequest):
20+
task_id = utils.get_uuid()
21+
request_id = base.get_task_id(request)
22+
try:
23+
task = {
24+
"task_id": task_id,
25+
"request_id": request_id,
26+
}
27+
body_dict = body.dict()
28+
task.update(body_dict)
29+
result = tm.start(task_id=task_id, params=body)
30+
task["result"] = result
31+
logger.success(f"video created: {utils.to_json(task)}")
32+
return utils.get_response(200, task)
33+
except ValueError as e:
34+
raise HttpException(task_id=task_id, status_code=400, message=f"{request_id}: {str(e)}")
35+
36+
37+
@router.get("/tasks/{task_id}", response_model=TaskQueryResponse, summary="查询任务状态")
38+
async def get_task(request: Request, task_id: str = Path(..., description="任务ID"),
39+
query: TaskQueryRequest = Depends()):
40+
request_id = base.get_task_id(request)
41+
data = query.dict()
42+
data["task_id"] = task_id
43+
raise HttpException(task_id=task_id, status_code=404,
44+
message=f"{request_id}: task not found", data=data)

app/models/__init__.py

Whitespace-only changes.

app/models/const.py

+4
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
punctuations = [
2+
"?", ",", ".", "、", ";",
3+
"?", ",", "。", "、", ";",
4+
]

app/models/exception.py

+26
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import traceback
2+
from typing import Any
3+
4+
from loguru import logger
5+
6+
7+
class HttpException(Exception):
8+
def __init__(self, task_id: str, status_code: int, message: str = '', data: Any = None):
9+
self.message = message
10+
self.status_code = status_code
11+
self.data = data
12+
# 获取异常堆栈信息
13+
tb_str = traceback.format_exc().strip()
14+
if not tb_str or tb_str == "NoneType: None":
15+
msg = f'HttpException: {status_code}, {task_id}, {message}'
16+
else:
17+
msg = f'HttpException: {status_code}, {task_id}, {message}\n{tb_str}'
18+
19+
if status_code == 400:
20+
logger.warning(msg)
21+
else:
22+
logger.error(msg)
23+
24+
25+
class FileNotFoundException(Exception):
26+
pass

0 commit comments

Comments
 (0)