refactor(webui): 优化音频设置界面并添加代理配置

- 修改支持的语音列表,仅保留中文语音
- 在主程序中添加代理配置环境变量
-优化剪辑视频函数,改为返回字典类型
- 更新任务服务中的剪辑视频函数,适应新的参数类型
- 修改测试用例中的视频剪辑函数,增加输出路径参数
- 更新脚本控制器中的剪辑视频函数,集成任务 ID 和子视频字典
This commit is contained in:
linyqh 2024-11-19 01:23:20 +08:00 committed by linyq
parent 1d5585e752
commit ee710499b9
10 changed files with 522 additions and 16 deletions

View File

@ -132,6 +132,8 @@ async def download_youtube_video(
)
async def start_subclip(
request: VideoClipParams,
task_id: str,
subclip_videos: dict,
background_tasks: BackgroundTasks
):
"""
@ -153,13 +155,13 @@ async def start_subclip(
# 在后台任务中执行视频剪辑
background_tasks.add_task(
task_service.start_subclip,
task_id=request.task_id,
task_id=task_id,
params=params,
subclip_path_videos=request.subclip_videos
subclip_path_videos=subclip_videos
)
return {
"task_id": request.task_id,
"task_id": task_id,
"state": "PROCESSING" # 初始状态
}

View File

@ -0,0 +1,162 @@
import requests
import json
import time
from typing import Dict, Any
class VideoPipeline:
def __init__(self, base_url: str = "http://127.0.0.1:8080"):
self.base_url = base_url
def download_video(self, url: str, resolution: str = "1080p",
output_format: str = "mp4", rename: str = None) -> Dict[str, Any]:
"""下载视频的第一步"""
endpoint = f"{self.base_url}/api/v2/youtube/download"
payload = {
"url": url,
"resolution": resolution,
"output_format": output_format,
"rename": rename or time.strftime("%Y-%m-%d")
}
response = requests.post(endpoint, json=payload)
response.raise_for_status()
return response.json()
def generate_script(self, video_path: str, skip_seconds: int = 0,
threshold: int = 30, vision_batch_size: int = 10,
vision_llm_provider: str = "gemini") -> Dict[str, Any]:
"""生成脚本的第二步"""
endpoint = f"{self.base_url}/api/v2/scripts/generate"
payload = {
"video_path": video_path,
"skip_seconds": skip_seconds,
"threshold": threshold,
"vision_batch_size": vision_batch_size,
"vision_llm_provider": vision_llm_provider
}
response = requests.post(endpoint, json=payload)
response.raise_for_status()
return response.json()
def crop_video(self, video_path: str, script: list) -> Dict[str, Any]:
"""剪辑视频的第三步"""
endpoint = f"{self.base_url}/api/v2/scripts/crop"
payload = {
"video_origin_path": video_path,
"video_script": script
}
response = requests.post(endpoint, json=payload)
response.raise_for_status()
return response.json()
def generate_final_video(self, task_id: str, video_path: str,
script_path: str, script: list, subclip_videos: Dict[str, str]) -> Dict[str, Any]:
"""生成最终视频的第四步"""
endpoint = f"{self.base_url}/api/v2/scripts/start-subclip"
request_data = {
"video_clip_json": script,
"video_clip_json_path": script_path,
"video_origin_path": video_path,
"video_aspect": "16:9",
"video_language": "zh-CN",
"voice_name": "zh-CN-YunjianNeural",
"voice_volume": 1,
"voice_rate": 1.2,
"voice_pitch": 1,
"bgm_name": "random",
"bgm_type": "random",
"bgm_file": "",
"bgm_volume": 0.3,
"subtitle_enabled": True,
"subtitle_position": "bottom",
"font_name": "STHeitiMedium.ttc",
"text_fore_color": "#FFFFFF",
"text_background_color": "transparent",
"font_size": 75,
"stroke_color": "#000000",
"stroke_width": 1.5,
"custom_position": 70,
"n_threads": 8
}
payload = {
"request": request_data,
"subclip_videos": subclip_videos
}
params = {"task_id": task_id}
response = requests.post(endpoint, params=params, json=payload)
response.raise_for_status()
return response.json()
def save_script_to_json(self, script: list) -> str:
"""保存脚本到json文件"""
timestamp = time.strftime("%Y-%m%d-%H%M%S")
script_path = f"E:\\projects\\NarratoAI\\resource\\scripts\\{timestamp}.json"
try:
with open(script_path, 'w', encoding='utf-8') as f:
json.dump(script, f, ensure_ascii=False, indent=2)
print(f"脚本已保存到: {script_path}")
return script_path
except Exception as e:
print(f"保存脚本失败: {str(e)}")
raise
def run_pipeline(self, youtube_url: str) -> Dict[str, Any]:
"""运行完整的pipeline"""
try:
# 1. 下载视频
print("开始下载视频...")
download_result = self.download_video(youtube_url)
video_path = download_result["output_path"]
# 2. 生成脚本
print("开始生成脚本...")
script_result = self.generate_script(video_path)
script = script_result["script"]
# 2.1 保存脚本到json文件
print("保存脚本到json文件...")
script_path = self.save_script_to_json(script)
script_result["script_path"] = script_path
# 3. 剪辑视频
print("开始剪辑视频...")
crop_result = self.crop_video(video_path, script)
subclip_videos = crop_result["subclip_videos"]
# 4. 生成最终视频
print("开始生成最终视频...")
final_result = self.generate_final_video(
crop_result["task_id"],
video_path,
script_path,
script,
subclip_videos
)
return {
"status": "success",
"download_result": download_result,
"script_result": script_result,
"crop_result": crop_result,
"final_result": final_result
}
except Exception as e:
return {
"status": "error",
"error": str(e)
}
# 使用示例
if __name__ == "__main__":
pipeline = VideoPipeline()
result = pipeline.run_pipeline("https://www.youtube.com/watch?v=Kenm35gdqtk")
print(json.dumps(result, indent=2, ensure_ascii=False))
result2 = pipeline.run_pipeline("https://www.youtube.com/watch?v=aEsHAcedzgw")
print(json.dumps(result2, indent=2, ensure_ascii=False))

View File

@ -363,7 +363,7 @@ def save_clip_video(timestamp: str, origin_video: str, save_dir: str = "") -> di
return {}
def clip_videos(task_id: str, timestamp_terms: List[str], origin_video: str, progress_callback=None):
def clip_videos(task_id: str, timestamp_terms: List[str], origin_video: str, progress_callback=None) -> dict:
"""
剪辑视频
Args:

View File

@ -324,7 +324,7 @@ def start(task_id, params: VideoParams, stop_at: str = "video"):
return kwargs
def start_subclip(task_id: str, params: VideoClipParams, subclip_path_videos: list):
def start_subclip(task_id: str, params: VideoClipParams, subclip_path_videos: dict):
"""
后台任务自动剪辑视频进行剪辑

View File

@ -989,6 +989,9 @@ Gender: Female
Name: zh-CN-XiaoxiaoMultilingualNeural-V2
Gender: Female
Name: zh-CN-YunxiNeural-V2
Gender: Male
""".strip()
voices = []
name = ""
@ -1034,8 +1037,8 @@ def is_azure_v2_voice(voice_name: str):
def tts(
text: str, voice_name: str, voice_rate: float, voice_pitch: float, voice_file: str
) -> [SubMaker, None]:
# if is_azure_v2_voice(voice_name):
# return azure_tts_v2(text, voice_name, voice_file)
if is_azure_v2_voice(voice_name):
return azure_tts_v2(text, voice_name, voice_file)
return azure_tts_v1(text, voice_name, voice_rate, voice_pitch, voice_file)

View File

@ -5,6 +5,7 @@ from loguru import logger
from uuid import uuid4
from app.utils import utils
from app.services import video as VideoService
class YoutubeService:
@ -61,6 +62,7 @@ class YoutubeService:
Args:
url: YouTube视频URL
resolution: 目标分辨率 ('2160p', '1440p', '1080p', '720p' etc.)
注意对于类似'1080p60'的输入会被处理为'1080p'
output_format: 输出视频格式
rename: 可选的重命名
@ -71,23 +73,32 @@ class YoutubeService:
task_id = str(uuid4())
self._validate_format(output_format)
# 标准化分辨率格式
base_resolution = resolution.split('p')[0] + 'p'
# 获取所有可用格式
formats = self._get_video_formats(url)
# 查找指定分辨率的最佳视频格式
target_format = None
for fmt in formats:
if fmt['resolution'] == resolution and fmt['vcodec'] != 'none':
target_format = fmt
break
fmt_resolution = fmt['resolution']
# 将格式的分辨率也标准化后进行比较
if fmt_resolution != 'N/A':
fmt_base_resolution = fmt_resolution.split('p')[0] + 'p'
if fmt_base_resolution == base_resolution and fmt['vcodec'] != 'none':
target_format = fmt
break
if target_format is None:
# 收集可用分辨率时也进行标准化
available_resolutions = set(
fmt['resolution'] for fmt in formats
fmt['resolution'].split('p')[0] + 'p'
for fmt in formats
if fmt['resolution'] != 'N/A' and fmt['vcodec'] != 'none'
)
raise ValueError(
f"未找到 {resolution} 分辨率的视频。"
f"未找到 {base_resolution} 分辨率的视频。"
f"可用分辨率: {', '.join(sorted(available_resolutions))}"
)

View File

@ -31,7 +31,7 @@ def format_duration(seconds: float) -> str:
return f"{minutes:02d}:{remaining_seconds:02d}"
def cut_video(video_path: str, start_time: str, end_time: str) -> None:
def cut_video(video_path: str, start_time: str, end_time: str, output_path: str) -> None:
"""
剪辑视频
参数:
@ -53,11 +53,13 @@ def cut_video(video_path: str, start_time: str, end_time: str) -> None:
# 剪辑视频
video = video.subclip(start_seconds, end_seconds)
video.write_videofile("../../resource/videos/cut_video2.mp4")
video.write_videofile("../../resource/videos/cut_video3.mp4")
# 释放资源
video.close()
if __name__ == "__main__":
cut_video("../../resource/videos/best.mp4", "00:40", "02:40")
# cut_video("E:\\NarratoAI_v0.3.5_cuda\\NarratoAI\storage\\tasks\ca4fee22-350b-47f9-bb2f-802ad96774f7\\final-2.mp4", "00:00", "07:00", "E:\\NarratoAI_v0.3.5_cuda\\NarratoAI\storage\\tasks\\yyjx2-1")
# cut_video("E:\\NarratoAI_v0.3.5_cuda\\NarratoAI\storage\\tasks\ca4fee22-350b-47f9-bb2f-802ad96774f7\\final-2.mp4", "07:00", "14:00", "E:\\NarratoAI_v0.3.5_cuda\\NarratoAI\storage\\tasks\\yyjx2-2")
cut_video("E:\\NarratoAI_v0.3.5_cuda\\NarratoAI\storage\\tasks\ca4fee22-350b-47f9-bb2f-802ad96774f7\\final-2.mp4", "14:00", "22:00", "E:\\NarratoAI_v0.3.5_cuda\\NarratoAI\storage\\tasks\\yyjx2-3")

View File

@ -1,3 +1,4 @@
import os
import uvicorn
from loguru import logger
@ -7,6 +8,8 @@ if __name__ == "__main__":
logger.info(
"start server, docs: http://127.0.0.1:" + str(config.listen_port) + "/docs"
)
os.environ["HTTP_PROXY"] = config.proxy.get("http")
os.environ["HTTPS_PROXY"] = config.proxy.get("https")
uvicorn.run(
app="app.asgi:app",
host=config.listen_host,

323
webui.txt
View File

@ -47,3 +47,326 @@ pause
rem set HF_ENDPOINT=https://hf-mirror.com
streamlit run webui.py --browser.serverAddress="127.0.0.1" --server.enableCORS=True --server.maxUploadSize=2048 --browser.gatherUsageStats=False
请求0
curl -X 'POST' \
'http://127.0.0.1:8080/api/v2/youtube/download' \
-H 'accept: application/json' \
-H 'Content-Type: application/json' \
-d '{
"url": "https://www.youtube.com/watch?v=Kenm35gdqtk",
"resolution": "1080p",
"output_format": "mp4",
"rename": "2024-11-19"
}'
{
"url": "https://www.youtube.com/watch?v=Kenm35gdqtk",
"resolution": "1080p",
"output_format": "mp4",
"rename": "2024-11-19"
}
请求1
curl -X 'POST' \
'http://127.0.0.1:8080/api/v2/scripts/generate' \
-H 'accept: application/json' \
-H 'Content-Type: application/json' \
-d '{
"video_path": "E:\\projects\\NarratoAI\\resource\\videos\\test.mp4",
"skip_seconds": 0,
"threshold": 30,
"vision_batch_size": 10,
"vision_llm_provider": "gemini"
}'
{
"video_path": "E:\\projects\\NarratoAI\\resource\\videos\\test.mp4",
"skip_seconds": 0,
"threshold": 30,
"vision_batch_size": 10,
"vision_llm_provider": "gemini"
}
请求2
curl -X 'POST' \
'http://127.0.0.1:8080/api/v2/scripts/crop' \
-H 'accept: application/json' \
-H 'Content-Type: application/json' \
-d '{
"video_origin_path": "E:\\projects\\NarratoAI\\resource\\videos\\test.mp4",
"video_script": [
{
"timestamp": "00:10-01:01",
"picture": "好的,以下是视频画面的客观描述:\n\n视频展现一名留着胡须的男子在森林里挖掘。\n\n画面首先展现男子从后方视角背着军绿色背包穿着卡其色长裤和深色T恤走向一个泥土斜坡。背包上似乎有一个镐头。\n\n下一个镜头特写展现了该背包一个镐头从背包里伸出来包里还有一些其他工具。\n\n然后视频显示该男子用镐头挖掘泥土斜坡。\n\n接下来是一些近景镜头展现男子的靴子在泥土中行走以及男子用手清理泥土。\n\n其他镜头从不同角度展现该男子在挖掘包括从侧面和上方。\n\n可以看到他用工具挖掘清理泥土并检查挖出的土壤。\n\n最后一个镜头展现了挖出的土壤的质地和颜色。",
"narration": "好的,接下来就是我们这位“胡须大侠”的精彩冒险了!只见他背着军绿色的背包,迈着比我上班还不情愿的步伐走向那泥土斜坡。哎呀,这个背包可真是个宝贝,里面藏着一把镐头和一些工具,简直像是个随身携带的“建筑工具箱”! \n\n看他挥舞着镐头挖掘泥土的姿势仿佛在进行一场“挖土大赛”结果却比我做饭还要糟糕。泥土飞扬中他的靴子也成了“泥巴艺术家”。最后那堆色泽各异的土壤就像他心情的写照——五彩斑斓又略显混乱真是一次让人捧腹的建造之旅",
"OST": 2,
"new_timestamp": "00:00-00:51"
},
{
"timestamp": "01:07-01:53",
"picture": "好的,以下是视频画面的客观描述:\n\n视频以一系列森林环境的镜头开头。\n\n第一个镜头是一个特写镜头镜头中显示的是一些带有水滴的绿色叶子。\n\n第二个镜头显示一个留着胡须的男子在森林中挖掘一个洞。 他跪在地上,用工具挖土。\n\n第三个镜头是一个中等镜头显示同一个人坐在他挖好的洞边休息。\n\n第四个镜头显示该洞的内部结构该洞在树根和地面之间。\n\n第五个镜头显示该男子用斧头砍树枝。\n\n第六个镜头显示一堆树枝横跨一个泥泞的小水坑。\n\n第七个镜头显示更多茂盛的树叶和树枝在阳光下。\n\n第八个镜头显示更多茂盛的树叶和树枝。\n\n\n",
"narration": "接下来,我们的“挖土大师”又开始了他的森林探险。看这镜头,水滴在叶子上闪烁,仿佛在说:“快来,快来,这里有故事!”他一边挖洞,一边像个新手厨师试图切洋葱——每一下都小心翼翼,生怕自己不小心挖出个“历史遗址”。坐下休息的时候,脸上的表情就像发现新大陆一样!然后,他拿起斧头砍树枝,简直是现代版的“神雕侠侣”,只不过对象是树木。最后,那堆树枝架过泥泞的小水坑,仿佛在说:“我就是不怕湿脚的勇士!”这就是我们的建造之旅!",
"OST": 2,
"new_timestamp": "00:51-01:37"
}
]
}'
{
"video_origin_path": "E:\\projects\\NarratoAI\\resource\\videos\\test.mp4",
"video_script": [
{
"timestamp": "00:10-01:01",
"picture": "好的,以下是视频画面的客观描述:\n\n视频展现一名留着胡须的男子在森林里挖掘。\n\n画面首先展现男子从后方视角背着军绿色背包穿着卡其色长裤和深色T恤走向一个泥土斜坡。背包上似乎有一个镐头。\n\n下一个镜头特写展现了该背包一个镐头从背包里伸出来包里还有一些其他工具。\n\n然后视频显示该男子用镐头挖掘泥土斜坡。\n\n接下来是一些近景镜头展现男子的靴子在泥土中行走以及男子用手清理泥土。\n\n其他镜头从不同角度展现该男子在挖掘包括从侧面和上方。\n\n可以看到他用工具挖掘清理泥土并检查挖出的土壤。\n\n最后一个镜头展现了挖出的土壤的质地和颜色。",
"narration": "好的,接下来就是我们这位“胡须大侠”的精彩冒险了!只见他背着军绿色的背包,迈着比我上班还不情愿的步伐走向那泥土斜坡。哎呀,这个背包可真是个宝贝,里面藏着一把镐头和一些工具,简直像是个随身携带的“建筑工具箱”! \n\n看他挥舞着镐头挖掘泥土的姿势仿佛在进行一场“挖土大赛”结果却比我做饭还要糟糕。泥土飞扬中他的靴子也成了“泥巴艺术家”。最后那堆色泽各异的土壤就像他心情的写照——五彩斑斓又略显混乱真是一次让人捧腹的建造之旅",
"OST": 2,
"new_timestamp": "00:00-00:51"
},
{
"timestamp": "01:07-01:53",
"picture": "好的,以下是视频画面的客观描述:\n\n视频以一系列森林环境的镜头开头。\n\n第一个镜头是一个特写镜头镜头中显示的是一些带有水滴的绿色叶子。\n\n第二个镜头显示一个留着胡须的男子在森林中挖掘一个洞。 他跪在地上,用工具挖土。\n\n第三个镜头是一个中等镜头显示同一个人坐在他挖好的洞边休息。\n\n第四个镜头显示该洞的内部结构该洞在树根和地面之间。\n\n第五个镜头显示该男子用斧头砍树枝。\n\n第六个镜头显示一堆树枝横跨一个泥泞的小水坑。\n\n第七个镜头显示更多茂盛的树叶和树枝在阳光下。\n\n第八个镜头显示更多茂盛的树叶和树枝。\n\n\n",
"narration": "接下来,我们的“挖土大师”又开始了他的森林探险。看这镜头,水滴在叶子上闪烁,仿佛在说:“快来,快来,这里有故事!”他一边挖洞,一边像个新手厨师试图切洋葱——每一下都小心翼翼,生怕自己不小心挖出个“历史遗址”。坐下休息的时候,脸上的表情就像发现新大陆一样!然后,他拿起斧头砍树枝,简直是现代版的“神雕侠侣”,只不过对象是树木。最后,那堆树枝架过泥泞的小水坑,仿佛在说:“我就是不怕湿脚的勇士!”这就是我们的建造之旅!",
"OST": 2,
"new_timestamp": "00:51-01:37"
}
]
}
请求3
curl -X 'POST' \
'http://127.0.0.1:8080/api/v2/scripts/start-subclip?task_id=12121' \
-H 'accept: application/json' \
-H 'Content-Type: application/json' \
-d '{
"request": {
"video_clip_json": [
{
"timestamp": "00:10-01:01",
"picture": "好的,以下是视频画面的客观描述:\n\n视频展现一名留着胡须的男子在森林里挖掘。\n\n画面首先展现男子从后方视角背着军绿色背包穿着卡其色长裤和深色T恤走向一个泥土斜坡。背包上似乎有一个镐头。\n\n下一个镜头特写展现了该背包一个镐头从背包里伸出来包里还有一些其他工具。\n\n然后视频显示该男子用镐头挖掘泥土斜坡。\n\n接下来是一些近景镜头展现男子的靴子在泥土中行走以及男子用手清理泥土。\n\n其他镜头从不同角度展现该男子在挖掘包括从侧面和上方。\n\n可以看到他用工具挖掘清理泥土并检查挖出的土壤。\n\n最后一个镜头展现了挖出的土壤的质地和颜色。",
"narration": "好的,接下来就是我们这位“胡须大侠”的精彩冒险了!只见他背着军绿色的背包,迈着比我上班还不情愿的步伐走向那泥土斜坡。哎呀,这个背包可真是个宝贝,里面藏着一把镐头和一些工具,简直像是个随身携带的“建筑工具箱”! \n\n看他挥舞着镐头挖掘泥土的姿势仿佛在进行一场“挖土大赛”结果却比我做饭还要糟糕。泥土飞扬中他的靴子也成了“泥巴艺术家”。最后那堆色泽各异的土壤就像他心情的写照——五彩斑斓又略显混乱真是一次让人捧腹的建造之旅",
"OST": 2,
"new_timestamp": "00:00-00:51"
},
{
"timestamp": "01:07-01:53",
"picture": "好的,以下是视频画面的客观描述:\n\n视频以一系列森林环境的镜头开头。\n\n第一个镜头是一个特写镜头镜头中显示的是一些带有水滴的绿色叶子。\n\n第二个镜头显示一个留着胡须的男子在森林中挖掘一个洞。 他跪在地上,用工具挖土。\n\n第三个镜头是一个中等镜头显示同一个人坐在他挖好的洞边休息。\n\n第四个镜头显示该洞的内部结构该洞在树根和地面之间。\n\n第五个镜头显示该男子用斧头砍树枝。\n\n第六个镜头显示一堆树枝横跨一个泥泞的小水坑。\n\n第七个镜头显示更多茂盛的树叶和树枝在阳光下。\n\n第八个镜头显示更多茂盛的树叶和树枝。\n\n\n",
"narration": "接下来,我们的“挖土大师”又开始了他的森林探险。看这镜头,水滴在叶子上闪烁,仿佛在说:“快来,快来,这里有故事!”他一边挖洞,一边像个新手厨师试图切洋葱——每一下都小心翼翼,生怕自己不小心挖出个“历史遗址”。坐下休息的时候,脸上的表情就像发现新大陆一样!然后,他拿起斧头砍树枝,简直是现代版的“神雕侠侣”,只不过对象是树木。最后,那堆树枝架过泥泞的小水坑,仿佛在说:“我就是不怕湿脚的勇士!”这就是我们的建造之旅!",
"OST": 2,
"new_timestamp": "00:51-01:37"
}
],
"video_clip_json_path": "E:\\projects\\NarratoAI\\resource\\scripts\\2024-1118-230421.json",
"video_origin_path": "E:\\projects\\NarratoAI\\resource\\videos\\test.mp4",
"video_aspect": "16:9",
"video_language": "zh-CN",
"voice_name": "zh-CN-YunjianNeural",
"voice_volume": 1,
"voice_rate": 1.2,
"voice_pitch": 1,
"bgm_name": "random",
"bgm_type": "random",
"bgm_file": "",
"bgm_volume": 0.3,
"subtitle_enabled": true,
"subtitle_position": "bottom",
"font_name": "STHeitiMedium.ttc",
"text_fore_color": "#FFFFFF",
"text_background_color": "transparent",
"font_size": 75,
"stroke_color": "#000000",
"stroke_width": 1.5,
"custom_position": 70,
"n_threads": 8
},
"subclip_videos": {
"00:10-01:01": "E:\\projects\\NarratoAI\\storage\\cache_videos/vid-00_10-01_01.mp4",
"01:07-01:53": "E:\\projects\\NarratoAI\\storage\\cache_videos/vid-01_07-01_53.mp4"
}
}'
{
"request": {
"video_clip_json": [
{
"timestamp": "00:10-01:01",
"picture": "好的,以下是视频画面的客观描述:\n\n视频展现一名留着胡须的男子在森林里挖掘。\n\n画面首先展现男子从后方视角背着军绿色背包穿着卡其色长裤和深色T恤走向一个泥土斜坡。背包上似乎有一个镐头。\n\n下一个镜头特写展现了该背包一个镐头从背包里伸出来包里还有一些其他工具。\n\n然后视频显示该男子用镐头挖掘泥土斜坡。\n\n接下来是一些近景镜头展现男子的靴子在泥土中行走以及男子用手清理泥土。\n\n其他镜头从不同角度展现该男子在挖掘包括从侧面和上方。\n\n可以看到他用工具挖掘清理泥土并检查挖出的土壤。\n\n最后一个镜头展现了挖出的土壤的质地和颜色。",
"narration": "好的,接下来就是我们这位“胡须大侠”的精彩冒险了!只见他背着军绿色的背包,迈着比我上班还不情愿的步伐走向那泥土斜坡。哎呀,这个背包可真是个宝贝,里面藏着一把镐头和一些工具,简直像是个随身携带的“建筑工具箱”! \n\n看他挥舞着镐头挖掘泥土的姿势仿佛在进行一场“挖土大赛”结果却比我做饭还要糟糕。泥土飞扬中他的靴子也成了“泥巴艺术家”。最后那堆色泽各异的土壤就像他心情的写照——五彩斑斓又略显混乱真是一次让人捧腹的建造之旅",
"OST": 2,
"new_timestamp": "00:00-00:51"
},
{
"timestamp": "01:07-01:53",
"picture": "好的,以下是视频画面的客观描述:\n\n视频以一系列森林环境的镜头开头。\n\n第一个镜头是一个特写镜头镜头中显示的是一些带有水滴的绿色叶子。\n\n第二个镜头显示一个留着胡须的男子在森林中挖掘一个洞。 他跪在地上,用工具挖土。\n\n第三个镜头是一个中等镜头显示同一个人坐在他挖好的洞边休息。\n\n第四个镜头显示该洞的内部结构该洞在树根和地面之间。\n\n第五个镜头显示该男子用斧头砍树枝。\n\n第六个镜头显示一堆树枝横跨一个泥泞的小水坑。\n\n第七个镜头显示更多茂盛的树叶和树枝在阳光下。\n\n第八个镜头显示更多茂盛的树叶和树枝。\n\n\n",
"narration": "接下来,我们的“挖土大师”又开始了他的森林探险。看这镜头,水滴在叶子上闪烁,仿佛在说:“快来,快来,这里有故事!”他一边挖洞,一边像个新手厨师试图切洋葱——每一下都小心翼翼,生怕自己不小心挖出个“历史遗址”。坐下休息的时候,脸上的表情就像发现新大陆一样!然后,他拿起斧头砍树枝,简直是现代版的“神雕侠侣”,只不过对象是树木。最后,那堆树枝架过泥泞的小水坑,仿佛在说:“我就是不怕湿脚的勇士!”这就是我们的建造之旅!",
"OST": 2,
"new_timestamp": "00:51-01:37"
}
],
"video_clip_json_path": "E:\\projects\\NarratoAI\\resource\\scripts\\2024-1118-230421.json",
"video_origin_path": "E:\\projects\\NarratoAI\\resource\\videos\\test.mp4",
"video_aspect": "16:9",
"video_language": "zh-CN",
"voice_name": "zh-CN-YunjianNeural",
"voice_volume": 1,
"voice_rate": 1.2,
"voice_pitch": 1,
"bgm_name": "random",
"bgm_type": "random",
"bgm_file": "",
"bgm_volume": 0.3,
"subtitle_enabled": true,
"subtitle_position": "bottom",
"font_name": "STHeitiMedium.ttc",
"text_fore_color": "#FFFFFF",
"text_background_color": "transparent",
"font_size": 75,
"stroke_color": "#000000",
"stroke_width": 1.5,
"custom_position": 70,
"n_threads": 8
},
"subclip_videos": {
"00:10-01:01": "E:\\projects\\NarratoAI\\storage\\cache_videos/vid-00_10-01_01.mp4",
"01:07-01:53": "E:\\projects\\NarratoAI\\storage\\cache_videos/vid-01_07-01_53.mp4"
}
}
请在最外层新建一个pipeline 工作流执行逻辑的代码;
他会按照下面的顺序请求接口
1.下载视频
curl -X 'POST' \
'http://127.0.0.1:8080/api/v2/youtube/download' \
-H 'accept: application/json' \
-H 'Content-Type: application/json' \
-d '{
"url": "https://www.youtube.com/watch?v=Kenm35gdqtk",
"resolution": "1080p",
"output_format": "mp4",
"rename": "2024-11-19"
}'
2.生成脚本
curl -X 'POST' \
'http://127.0.0.1:8080/api/v2/scripts/generate' \
-H 'accept: application/json' \
-H 'Content-Type: application/json' \
-d '{
"video_path": "E:\\projects\\NarratoAI\\resource\\videos\\test.mp4",
"skip_seconds": 0,
"threshold": 30,
"vision_batch_size": 10,
"vision_llm_provider": "gemini"
}'
3. 剪辑视频
curl -X 'POST' \
'http://127.0.0.1:8080/api/v2/scripts/crop' \
-H 'accept: application/json' \
-H 'Content-Type: application/json' \
-d '{
"video_origin_path": "E:\\projects\\NarratoAI\\resource\\videos\\test.mp4",
"video_script": [
{
"timestamp": "00:10-01:01",
"picture": "好的,以下是视频画面的客观描述:\n\n视频展现一名留着胡须的男子在森林里挖掘。\n\n画面首先展现男子从后方视角背着军绿色背包穿着卡其色长裤和深色T恤走向一个泥土斜坡。背包上似乎有一个镐头。\n\n下一个镜头特写展现了该背包一个镐头从背包里伸出来包里还有一些其他工具。\n\n然后视频显示该男子用镐头挖掘泥土斜坡。\n\n接下来是一些近景镜头展现男子的靴子在泥土中行走以及男子用手清理泥土。\n\n其他镜头从不同角度展现该男子在挖掘包括从侧面和上方。\n\n可以看到他用工具挖掘清理泥土并检查挖出的土壤。\n\n最后一个镜头展现了挖出的土壤的质地和颜色。",
"narration": "好的,接下来就是我们这位“胡须大侠”的精彩冒险了!只见他背着军绿色的背包,迈着比我上班还不情愿的步伐走向那泥土斜坡。哎呀,这个背包可真是个宝贝,里面藏着一把镐头和一些工具,简直像是个随身携带的“建筑工具箱”! \n\n看他挥舞着镐头挖掘泥土的姿势仿佛在进行一场“挖土大赛”结果却比我做饭还要糟糕。泥土飞扬中他的靴子也成了“泥巴艺术家”。最后那堆色泽各异的土壤就像他心情的写照——五彩斑斓又略显混乱真是一次让人捧腹的建造之旅",
"OST": 2,
"new_timestamp": "00:00-00:51"
},
{
"timestamp": "01:07-01:53",
"picture": "好的,以下是视频画面的客观描述:\n\n视频以一系列森林环境的镜头开头。\n\n第一个镜头是一个特写镜头镜头中显示的是一些带有水滴的绿色叶子。\n\n第二个镜头显示一个留着胡须的男子在森林中挖掘一个洞。 他跪在地上,用工具挖土。\n\n第三个镜头是一个中等镜头显示同一个人坐在他挖好的洞边休息。\n\n第四个镜头显示该洞的内部结构该洞在树根和地面之间。\n\n第五个镜头显示该男子用斧头砍树枝。\n\n第六个镜头显示一堆树枝横跨一个泥泞的小水坑。\n\n第七个镜头显示更多茂盛的树叶和树枝在阳光下。\n\n第八个镜头显示更多茂盛的树叶和树枝。\n\n\n",
"narration": "接下来,我们的“挖土大师”又开始了他的森林探险。看这镜头,水滴在叶子上闪烁,仿佛在说:“快来,快来,这里有故事!”他一边挖洞,一边像个新手厨师试图切洋葱——每一下都小心翼翼,生怕自己不小心挖出个“历史遗址”。坐下休息的时候,脸上的表情就像发现新大陆一样!然后,他拿起斧头砍树枝,简直是现代版的“神雕侠侣”,只不过对象是树木。最后,那堆树枝架过泥泞的小水坑,仿佛在说:“我就是不怕湿脚的勇士!”这就是我们的建造之旅!",
"OST": 2,
"new_timestamp": "00:51-01:37"
}
]
}'
4.生成视频
curl -X 'POST' \
'http://127.0.0.1:8080/api/v2/scripts/start-subclip?task_id=12121' \
-H 'accept: application/json' \
-H 'Content-Type: application/json' \
-d '{
"request": {
"video_clip_json": [
{
"timestamp": "00:10-01:01",
"picture": "好的,以下是视频画面的客观描述:\n\n视频展现一名留着胡须的男子在森林里挖掘。\n\n画面首先展现男子从后方视角背着军绿色背包穿着卡其色长裤和深色T恤走向一个泥土斜坡。背包上似乎有一个镐头。\n\n下一个镜头特写展现了该背包一个镐头从背包里伸出来包里还有一些其他工具。\n\n然后视频显示该男子用镐头挖掘泥土斜坡。\n\n接下来是一些近景镜头展现男子的靴子在泥土中行走以及男子用手清理泥土。\n\n其他镜头从不同角度展现该男子在挖掘包括从侧面和上方。\n\n可以看到他用工具挖掘清理泥土并检查挖出的土壤。\n\n最后一个镜头展现了挖出的土壤的质地和颜色。",
"narration": "好的,接下来就是我们这位“胡须大侠”的精彩冒险了!只见他背着军绿色的背包,迈着比我上班还不情愿的步伐走向那泥土斜坡。哎呀,这个背包可真是个宝贝,里面藏着一把镐头和一些工具,简直像是个随身携带的“建筑工具箱”! \n\n看他挥舞着镐头挖掘泥土的姿势仿佛在进行一场“挖土大赛”结果却比我做饭还要糟糕。泥土飞扬中他的靴子也成了“泥巴艺术家”。最后那堆色泽各异的土壤就像他心情的写照——五彩斑斓又略显混乱真是一次让人捧腹的建造之旅",
"OST": 2,
"new_timestamp": "00:00-00:51"
},
{
"timestamp": "01:07-01:53",
"picture": "好的,以下是视频画面的客观描述:\n\n视频以一系列森林环境的镜头开头。\n\n第一个镜头是一个特写镜头镜头中显示的是一些带有水滴的绿色叶子。\n\n第二个镜头显示一个留着胡须的男子在森林中挖掘一个洞。 他跪在地上,用工具挖土。\n\n第三个镜头是一个中等镜头显示同一个人坐在他挖好的洞边休息。\n\n第四个镜头显示该洞的内部结构该洞在树根和地面之间。\n\n第五个镜头显示该男子用斧头砍树枝。\n\n第六个镜头显示一堆树枝横跨一个泥泞的小水坑。\n\n第七个镜头显示更多茂盛的树叶和树枝在阳光下。\n\n第八个镜头显示更多茂盛的树叶和树枝。\n\n\n",
"narration": "接下来,我们的“挖土大师”又开始了他的森林探险。看这镜头,水滴在叶子上闪烁,仿佛在说:“快来,快来,这里有故事!”他一边挖洞,一边像个新手厨师试图切洋葱——每一下都小心翼翼,生怕自己不小心挖出个“历史遗址”。坐下休息的时候,脸上的表情就像发现新大陆一样!然后,他拿起斧头砍树枝,简直是现代版的“神雕侠侣”,只不过对象是树木。最后,那堆树枝架过泥泞的小水坑,仿佛在说:“我就是不怕湿脚的勇士!”这就是我们的建造之旅!",
"OST": 2,
"new_timestamp": "00:51-01:37"
}
],
"video_clip_json_path": "E:\\projects\\NarratoAI\\resource\\scripts\\2024-1118-230421.json",
"video_origin_path": "E:\\projects\\NarratoAI\\resource\\videos\\test.mp4",
"video_aspect": "16:9",
"video_language": "zh-CN",
"voice_name": "zh-CN-YunjianNeural",
"voice_volume": 1,
"voice_rate": 1.2,
"voice_pitch": 1,
"bgm_name": "random",
"bgm_type": "random",
"bgm_file": "",
"bgm_volume": 0.3,
"subtitle_enabled": true,
"subtitle_position": "bottom",
"font_name": "STHeitiMedium.ttc",
"text_fore_color": "#FFFFFF",
"text_background_color": "transparent",
"font_size": 75,
"stroke_color": "#000000",
"stroke_width": 1.5,
"custom_position": 70,
"n_threads": 8
},
"subclip_videos": {
"00:10-01:01": "E:\\projects\\NarratoAI\\storage\\cache_videos/vid-00_10-01_01.mp4",
"01:07-01:53": "E:\\projects\\NarratoAI\\storage\\cache_videos/vid-01_07-01_53.mp4"
}
}'
请求1返回的参数是
{
"task_id": "4e9b575f-68c0-4ae1-b218-db42b67993d0",
"output_path": "E:\\projects\\NarratoAI\\resource\\videos\\2024-11-19.mp4",
"resolution": "1080p",
"format": "mp4",
"filename": "2024-11-19.mp4"
}
output_path需要传递给请求2
请求2返回数据为
{
"task_id": "04497017-953c-44b4-bf1d-9d8ed3ebbbce",
"script": [
{
"timestamp": "00:10-01:01",
"picture": "好的,以下是對影片畫面的客觀描述:\n\n影片顯示一名留著鬍鬚的男子在一處樹林茂密的斜坡上挖掘。\n\n畫面一男子從後方出現背著一個軍綠色的背包背包裡似乎裝有工具。他穿著卡其色的長褲和深色的登山鞋。\n\n畫面二特寫鏡頭顯示男子的背包一個舊的鎬頭從包裡露出來包裡還有其他工具包括一個鏟子。\n\n畫面三男子用鎬頭在斜坡上挖土背包放在他旁邊。\n\n畫面四特寫鏡頭顯示男子的登山鞋在泥土中。\n\n畫面五男子坐在斜坡上用手清理樹根和泥土。\n\n畫面六地上有一些鬆動的泥土和落葉。\n\n畫面七男子的背包近景鏡頭他正在挖掘。\n\n畫面八男子在斜坡上挖掘揚起一陣塵土。\n\n畫面九特寫鏡頭顯示男子用手清理泥土。\n\n畫面十特寫鏡頭顯示挖出的泥土剖面可以看到土壤的層次。",
"narration": "上一个画面是我在绝美的自然中,准备开启我的“土豪”挖掘之旅。现在,你们看到这位留着胡子的“大哥”,他背着个军绿色的包,里面装的可不仅仅是工具,还有我对生活的无限热爱(以及一丝不安)。看!这把旧镐头就像我的前任——用起来费劲,但又舍不得扔掉。\n\n他在斜坡上挖土泥土飞扬仿佛在跟大地进行一场“泥巴大战”。每一铲下去都能听到大地微微的呻吟哎呀我这颗小树根可比我当年的情感纠葛还难处理呢别担心这些泥土层次分明简直可以开个“泥土博物馆”。所以朋友们跟着我一起享受这场泥泞中的乐趣吧",
"OST": 2,
"new_timestamp": "00:00-00:51"
},
{
"timestamp": "01:07-01:53",
"picture": "好的,以下是對影片畫面內容的客觀描述:\n\n影片以一系列森林環境的鏡頭開始。第一個鏡頭展示了綠葉植物的特寫鏡頭葉子上有一些水珠。接下來的鏡頭是一個男人在森林裡挖掘一個小坑他跪在地上用鏟子挖土。\n\n接下來的鏡頭是同一個男人坐在他挖的坑旁邊望著前方。然後鏡頭顯示該坑的廣角鏡頭顯示其結構和大小。\n\n之後的鏡頭同一個男人在樹林裡劈柴。鏡頭最後呈現出一潭渾濁的水周圍環繞著樹枝。然後鏡頭又回到了森林裡生長茂盛的植物特寫鏡頭。",
"narration": "好嘞,朋友们,我们已经在泥土博物馆里捣鼓了一阵子,现在是时候跟大自然亲密接触了!看看这片森林,绿叶上水珠闪闪发光,就像我曾经的爱情,虽然短暂,却美得让人心碎。\n\n现在我在这里挖个小坑感觉自己就像是一位新晋“挖土大王”不过说实话这手艺真不敢恭维连铲子都快对我崩溃了。再说劈柴这动作简直比我前任的情绪波动还要激烈最后这一潭浑浊的水别担心它只是告诉我生活就像这水总有些杂质但也别忘了要勇敢面对哦",
"OST": 2,
"new_timestamp": "00:51-01:37"
}
]
}
output_path和script参数需要传递给请求3
请求3返回参数是
{
"task_id": "b6f5a98a-b2e0-4e3d-89c5-64fb90db2ec1",
"subclip_videos": {
"00:10-01:01": "E:\\projects\\NarratoAI\\storage\\cache_videos/vid-00_10-01_01.mp4",
"01:07-01:53": "E:\\projects\\NarratoAI\\storage\\cache_videos/vid-01_07-01_53.mp4"
}
}
subclip_videos和 output_path和script参数需要传递给请求4
最后完成工作流

View File

@ -20,7 +20,7 @@ def render_audio_panel(tr):
def render_tts_settings(tr):
"""渲染TTS(文本转语音)设置"""
# 获取支持的语音列表
support_locales = ["zh-CN", "zh-HK", "zh-TW", "en-US"]
support_locales = ["zh-CN"]
voices = voice.get_all_azure_voices(filter_locals=support_locales)
# 创建友好的显示名称