Compare commits
43 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
414bcb0621 | ||
|
|
d4eb7bc333 | ||
|
|
1e96357f00 | ||
|
|
2e58d7ccf2 | ||
|
|
176660b442 | ||
|
|
b9b9bea2a6 | ||
|
|
17df9a1f27 | ||
|
|
00052b4c50 | ||
|
|
b8369349ea | ||
|
|
3de3e19276 | ||
|
|
bd33419460 | ||
|
|
d13a3cf6e9 | ||
|
|
3e4d5f52fd | ||
|
|
9a1ee9abfb | ||
|
|
2c41e6be62 | ||
|
|
a17d52c1ae | ||
|
|
b1506b9161 | ||
|
|
53923e0d25 | ||
|
|
1a302a1791 | ||
|
|
ce0f557702 | ||
|
|
a8d208bdc3 | ||
|
|
0cb71d6218 | ||
|
|
52b92d175d | ||
|
|
76e1407d9b | ||
|
|
26437a666c | ||
|
|
8907958fec | ||
|
|
0550e433d1 | ||
|
|
1fb3399b02 | ||
|
|
9ab13a74a2 | ||
|
|
1dbfcfadab | ||
|
|
ee7306d216 | ||
|
|
a8b54415a5 | ||
|
|
7a8e25dc36 | ||
|
|
c8adc453ae | ||
|
|
24a9ca514e | ||
|
|
a7466b2393 | ||
|
|
1f2b36a4f0 | ||
|
|
91218ecf95 | ||
|
|
a0a5a4059f | ||
|
|
90f0f560b2 | ||
|
|
05da4a3766 | ||
|
|
066e33def9 | ||
|
|
bb66b7e10c |
@@ -21,3 +21,4 @@ __pycache__/
|
|||||||
.svn/
|
.svn/
|
||||||
|
|
||||||
storage/
|
storage/
|
||||||
|
config.toml
|
||||||
|
|||||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -10,3 +10,5 @@
|
|||||||
/*/__pycache__/*
|
/*/__pycache__/*
|
||||||
.vscode
|
.vscode
|
||||||
/**/.streamlit
|
/**/.streamlit
|
||||||
|
__pycache__
|
||||||
|
logs/
|
||||||
43
README.md
43
README.md
@@ -66,6 +66,9 @@
|
|||||||
- [ ] 支持更多的语音合成服务商,比如 OpenAI TTS, Azure TTS
|
- [ ] 支持更多的语音合成服务商,比如 OpenAI TTS, Azure TTS
|
||||||
- [ ] 自动上传到YouTube平台
|
- [ ] 自动上传到YouTube平台
|
||||||
|
|
||||||
|
## 交流讨论 💬
|
||||||
|
<img src="docs/wechat-03.jpg" width="300">
|
||||||
|
|
||||||
## 视频演示 📺
|
## 视频演示 📺
|
||||||
|
|
||||||
### 竖屏 9:16
|
### 竖屏 9:16
|
||||||
@@ -102,8 +105,17 @@
|
|||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
|
## 配置要求 📦
|
||||||
|
- 建议最低 CPU 4核或以上,内存 8G 或以上,显卡非必须
|
||||||
|
- Windows 10 或 MacOS 11.0 以上系统
|
||||||
|
|
||||||
## 安装部署 📥
|
## 安装部署 📥
|
||||||
|
|
||||||
|
> 不想部署的可以直接下载安装包,解压直接使用
|
||||||
|
- **Windows** 版本下载地址
|
||||||
|
- 百度网盘: https://pan.baidu.com/s/1BB3SGtAFTytzFLS5t2d8Gg?pwd=5bry
|
||||||
|
|
||||||
|
### 前提条件
|
||||||
- 尽量不要使用 **中文路径**,避免出现一些无法预料的问题
|
- 尽量不要使用 **中文路径**,避免出现一些无法预料的问题
|
||||||
- 请确保你的 **网络** 是正常的,VPN需要打开`全局流量`模式
|
- 请确保你的 **网络** 是正常的,VPN需要打开`全局流量`模式
|
||||||
|
|
||||||
@@ -230,8 +242,8 @@ python main.py
|
|||||||
|
|
||||||
当前支持2种字幕生成方式:
|
当前支持2种字幕生成方式:
|
||||||
|
|
||||||
- edge: 生成速度更快,性能更好,对电脑配置没有要求,但是质量可能不稳定
|
- **edge**: 生成`速度快`,性能更好,对电脑配置没有要求,但是质量可能不稳定
|
||||||
- whisper: 生成速度较慢,性能较差,对电脑配置有一定要求,但是质量更可靠。
|
- **whisper**: 生成`速度慢`,性能较差,对电脑配置有一定要求,但是`质量更可靠`。
|
||||||
|
|
||||||
可以修改 `config.toml` 配置文件中的 `subtitle_provider` 进行切换
|
可以修改 `config.toml` 配置文件中的 `subtitle_provider` 进行切换
|
||||||
|
|
||||||
@@ -241,6 +253,25 @@ python main.py
|
|||||||
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` 目录下。
|
||||||
@@ -375,14 +406,6 @@ 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"> |
|
|
||||||
|
|
||||||
## 参考项目 📚
|
## 参考项目 📚
|
||||||
|
|
||||||
|
|||||||
@@ -36,6 +36,8 @@ def save_config():
|
|||||||
_cfg["app"] = app
|
_cfg["app"] = app
|
||||||
_cfg["whisper"] = whisper
|
_cfg["whisper"] = whisper
|
||||||
_cfg["pexels"] = pexels
|
_cfg["pexels"] = pexels
|
||||||
|
_cfg["azure"] = azure
|
||||||
|
_cfg["ui"] = ui
|
||||||
f.write(toml.dumps(_cfg))
|
f.write(toml.dumps(_cfg))
|
||||||
|
|
||||||
|
|
||||||
@@ -43,6 +45,7 @@ _cfg = load_config()
|
|||||||
app = _cfg.get("app", {})
|
app = _cfg.get("app", {})
|
||||||
whisper = _cfg.get("whisper", {})
|
whisper = _cfg.get("whisper", {})
|
||||||
pexels = _cfg.get("pexels", {})
|
pexels = _cfg.get("pexels", {})
|
||||||
|
azure = _cfg.get("azure", {})
|
||||||
ui = _cfg.get("ui", {})
|
ui = _cfg.get("ui", {})
|
||||||
|
|
||||||
hostname = socket.gethostname()
|
hostname = socket.gethostname()
|
||||||
@@ -53,7 +56,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.2")
|
||||||
reload_debug = False
|
reload_debug = False
|
||||||
|
|
||||||
imagemagick_path = app.get("imagemagick_path", "")
|
imagemagick_path = app.get("imagemagick_path", "")
|
||||||
@@ -63,3 +66,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}")
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
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
|
||||||
|
|
||||||
@@ -78,7 +80,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 +91,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 +132,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:]}')
|
||||||
|
|||||||
@@ -59,6 +59,11 @@ 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 = "***"
|
||||||
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 +76,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}]
|
||||||
)
|
)
|
||||||
content = response["output"]["text"]
|
if response:
|
||||||
return content.replace("\n", "")
|
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"]
|
||||||
|
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 +132,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(
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -105,7 +106,19 @@ def save_video(video_url: str, save_dir: str = "") -> str:
|
|||||||
f.write(requests.get(video_url, proxies=proxies, verify=False, timeout=(60, 240)).content)
|
f.write(requests.get(video_url, proxies=proxies, 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:
|
||||||
return video_path
|
try:
|
||||||
|
clip = VideoFileClip(video_path)
|
||||||
|
duration = clip.duration
|
||||||
|
fps = clip.fps
|
||||||
|
clip.close()
|
||||||
|
if duration > 0 and fps > 0:
|
||||||
|
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 ""
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -13,15 +13,16 @@ 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 ""
|
||||||
|
|
||||||
|
|
||||||
@@ -99,16 +100,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 +129,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 +153,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 +170,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,19 +247,24 @@ 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:
|
||||||
bgm_clip = (AudioFileClip(bgm_file)
|
try:
|
||||||
.set_duration(video_clip.duration)
|
bgm_clip = (AudioFileClip(bgm_file)
|
||||||
.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])
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"failed to add bgm: {str(e)}")
|
||||||
|
|
||||||
audio_clip = CompositeAudioClip([audio_clip, bgm_clip])
|
|
||||||
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")
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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,10 +9,11 @@ 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"]
|
||||||
voices_str = """
|
voices_str = """
|
||||||
@@ -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)
|
||||||
|
print(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]:
|
||||||
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")
|
||||||
|
|||||||
@@ -163,12 +163,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
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -162,3 +162,9 @@
|
|||||||
### Doc: https://requests.readthedocs.io/en/latest/user/advanced/#proxies
|
### Doc: https://requests.readthedocs.io/en/latest/user/advanced/#proxies
|
||||||
# http = "http://10.10.1.10:3128"
|
# http = "http://10.10.1.10:3128"
|
||||||
# https = "http://10.10.1.10:1080"
|
# https = "http://10.10.1.10:1080"
|
||||||
|
|
||||||
|
[azure]
|
||||||
|
# Azure Speech API Key
|
||||||
|
# Get your API key at https://portal.azure.com/#view/Microsoft_Azure_ProjectOxford/CognitiveServicesHub/~/SpeechServices
|
||||||
|
speech_key=""
|
||||||
|
speech_region=""
|
||||||
@@ -1,5 +1,3 @@
|
|||||||
version: "3"
|
|
||||||
|
|
||||||
x-common-volumes: &common-volumes
|
x-common-volumes: &common-volumes
|
||||||
- ./:/MoneyPrinterTurbo
|
- ./:/MoneyPrinterTurbo
|
||||||
|
|
||||||
|
|||||||
BIN
docs/wechat-03.jpg
Normal file
BIN
docs/wechat-03.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 165 KiB |
@@ -17,3 +17,9 @@ 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
|
||||||
|
# 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
|
||||||
@@ -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
|
||||||
@@ -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__)))
|
||||||
@@ -62,6 +63,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
|
||||||
|
|
||||||
|
|
||||||
@@ -164,7 +166,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 +176,7 @@ 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', '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):
|
||||||
@@ -190,6 +191,7 @@ with st.expander(tr("Basic Settings"), expanded=False):
|
|||||||
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", "")
|
||||||
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)
|
||||||
@@ -200,7 +202,10 @@ 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", [])
|
pexels_api_keys = config.app.get("pexels_api_keys", [])
|
||||||
@@ -212,7 +217,6 @@ with st.expander(tr("Basic Settings"), expanded=False):
|
|||||||
pexels_api_key = pexels_api_key.replace(" ", "")
|
pexels_api_key = pexels_api_key.replace(" ", "")
|
||||||
if pexels_api_key:
|
if pexels_api_key:
|
||||||
config.app["pexels_api_keys"] = pexels_api_key.split(",")
|
config.app["pexels_api_keys"] = pexels_api_key.split(",")
|
||||||
config.save_config()
|
|
||||||
|
|
||||||
panel = st.columns(3)
|
panel = st.columns(3)
|
||||||
left_panel = panel[0]
|
left_panel = panel[0]
|
||||||
@@ -295,20 +299,20 @@ 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"])
|
voices = voice.get_all_azure_voices(filter_locals=["zh-CN", "zh-HK", "zh-TW", "de-DE", "en-US", "fr-FR"])
|
||||||
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()):
|
||||||
saved_voice_name_index = i
|
saved_voice_name_index = i
|
||||||
break
|
break
|
||||||
|
|
||||||
@@ -319,7 +323,13 @@ 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 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)
|
||||||
@@ -356,7 +366,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"),
|
||||||
@@ -439,3 +448,5 @@ if start_button:
|
|||||||
open_task_folder(task_id)
|
open_task_folder(task_id)
|
||||||
logger.info(tr("Video Generation Completed"))
|
logger.info(tr("Video Generation Completed"))
|
||||||
scroll_to_bottom()
|
scroll_to_bottom()
|
||||||
|
|
||||||
|
config.save_config()
|
||||||
|
|||||||
@@ -23,6 +23,8 @@
|
|||||||
"Number of Videos Generated Simultaneously": "Anzahl der parallel generierten Videos",
|
"Number of Videos Generated Simultaneously": "Anzahl der parallel generierten Videos",
|
||||||
"Audio Settings": "**Audio Einstellungen**",
|
"Audio Settings": "**Audio Einstellungen**",
|
||||||
"Speech Synthesis": "Sprachausgabe",
|
"Speech Synthesis": "Sprachausgabe",
|
||||||
|
"Speech Region": "Region(:red[Required,[Get Region](https://portal.azure.com/#view/Microsoft_Azure_ProjectOxford/CognitiveServicesHub/~/SpeechServices)])",
|
||||||
|
"Speech Key": "API Key(:red[Required,[Get API Key](https://portal.azure.com/#view/Microsoft_Azure_ProjectOxford/CognitiveServicesHub/~/SpeechServices)])",
|
||||||
"Speech Volume": "Lautstärke der Sprachausgabe",
|
"Speech Volume": "Lautstärke der Sprachausgabe",
|
||||||
"Male": "Männlich",
|
"Male": "Männlich",
|
||||||
"Female": "Weiblich",
|
"Female": "Weiblich",
|
||||||
|
|||||||
@@ -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",
|
||||||
@@ -55,6 +57,7 @@
|
|||||||
"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**",
|
||||||
|
|||||||
@@ -23,6 +23,8 @@
|
|||||||
"Number of Videos Generated Simultaneously": "同时生成视频数量",
|
"Number of Videos Generated Simultaneously": "同时生成视频数量",
|
||||||
"Audio Settings": "**音频设置**",
|
"Audio Settings": "**音频设置**",
|
||||||
"Speech Synthesis": "朗读声音(:red[尽量与文案语言保持一致])",
|
"Speech Synthesis": "朗读声音(:red[尽量与文案语言保持一致])",
|
||||||
|
"Speech Region": "服务区域(:red[必填,[点击获取](https://portal.azure.com/#view/Microsoft_Azure_ProjectOxford/CognitiveServicesHub/~/SpeechServices)])",
|
||||||
|
"Speech Key": "API Key(:red[必填,[点击获取](https://portal.azure.com/#view/Microsoft_Azure_ProjectOxford/CognitiveServicesHub/~/SpeechServices)])",
|
||||||
"Speech Volume": "朗读音量(1.0表示100%)",
|
"Speech Volume": "朗读音量(1.0表示100%)",
|
||||||
"Male": "男性",
|
"Male": "男性",
|
||||||
"Female": "女性",
|
"Female": "女性",
|
||||||
@@ -55,6 +57,7 @@
|
|||||||
"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**",
|
||||||
|
|||||||
Reference in New Issue
Block a user