完成优化webui体验-剪辑逻辑进度90%;

待优化点:
1. 优化脚本-解说质量
This commit is contained in:
linyq 2024-09-29 18:34:36 +08:00
parent dc4ce80ea5
commit decac3b11d
7 changed files with 317 additions and 273 deletions

View File

@ -31,7 +31,7 @@ Method = """
文案的前三句是整部电影的概括总结2-3句介绍后开始叙述故事剧情 文案的前三句是整部电影的概括总结2-3句介绍后开始叙述故事剧情
推荐新手新号盘点型 推荐新手新号盘点型
盘点全球最恐怖的10部电影 盘点全球最恐怖的10部电影
全球最科幻的10部电影 <EFBFBD><EFBFBD><EFBFBD>全球最科幻的10部电影
盘点全球最悲惨的10部电影 盘点全球最悲惨的10部电影
盘全球最值得看的10部灾难电影 盘全球最值得看的10部灾难电影
盘点全球最值得看的10部励志电影 盘点全球最值得看的10部励志电影
@ -43,13 +43,13 @@ Method = """
4.是什么样的一个人被豆瓣网友称之为史上最牛P的老太太都70岁了还要去贩毒 4.是什么样的一个人被豆瓣网友称之为史上最牛P的老太太都70岁了还要去贩毒
5.他是M国历史上最NB//猖狂/冤枉的囚犯/抢劫犯/ 5.他是M国历史上最NB//猖狂/冤枉的囚犯/抢劫犯/
6.这到底是一部什么样的影片他一个人就拿了4个顶级奖项第一季8.7第二季直接干到9.511万人给出5星好评一共也就6集却斩获26项国际大奖看过的人都说他是近年来最好的xxx剧几乎成为了近年来xxx剧的标杆故事发生在 6.这到底是一部什么样的影片他一个人就拿了4个顶级奖项第一季8.7第二季直接干到9.511万人给出5星好评一共也就6集却斩获26项国际大奖看过的人都说他是近年来最好的xxx剧几乎成为了近年来xxx剧的标杆故事发生在
7.他是国产电影的巅峰佳作更是许多80-90后的青春启蒙曾入选周刊获得年度佳片第一可在国内却被尘封多年至今为止都无法在各大视频网站看到完整资源他就是xxxxxx 7.他是国产电影的巅峰佳作更是许多80-90后的青春启蒙曾入选<EFBFBD><EFBFBD>周刊获得年度佳片第一可在国内却被尘封多年至今为止都无法在各大视频网站看到完整资源他就是xxxxxx
8.这是一部让所有人看得荷尔蒙飙升的爽片 8.这是一部让所有人看得荷尔蒙飙升的爽片
9.他被成为世界上最虐心绝望的电影至今无人敢看第二遍很难想象他是根据真实事件改编而来 9.他被成为世界上最虐心绝望的电影至今无人敢看第二遍很难想象他是根据真实事件改编而来
10.这大概是有史以来最令人不寒而栗的电影当年一经放映就点燃了无数人的怒火不少观众不等影片放完就愤然离场它比xxx更让人绝望比比xxx更让人xxx能坚持看完全片的人更是万中无一包括我甚至观影结束后有无数人抵制投诉这部电影认为影片的导演玩弄了他们的情感他是顶级神作xxxx 10.这大概是有史以来最令人不寒而栗的电影当年一经放映就点燃了无数人的怒火不少观众不等影片放完就愤然离场它比xxx更让人绝望比比xxx更让人xxx能坚持看完全片的人更是万中无一包括我甚至观影结束后有无数人抵制投诉这部电影认为影片的导演玩弄了他们的情感他是顶级神作xxxx
11.这是X国有史以来最高赞的一部悬疑电影然而却因为某些原因国内90%的人没能看过这部片子他就是xxx 11.这是X国有史以来最高赞的一部悬疑电影然而却因为某些原因国内90%的人没能看过这部片子他就是xxx
12.有这样一部电影这辈子你绝对不想再看第二遍并不是它剧情烂俗而是它的结局你根本承受不起/想象不到甚至有80%的观众在观影途中情绪崩溃中途离场更让许多同行都不想解说这部电影他就是大名鼎鼎的暗黑神作xxx 12.有这样一部电影这辈子你绝对不想再看第二遍并不是它剧情烂俗而是它的结局你根本承受不起/想象不到甚至有80%的观众在观影途中情绪崩溃中途离场更让许多同行都不想解说这部电影他就是大名鼎鼎的暗黑神作xxx
13.它被誉为史上最牛悬疑片无数人在看完它时候一个月不敢照镜这样一部仅适合部分年龄段观看的影片究竟有什么样的魅力竟然获得某瓣8.2的高分很多人说这部电影到处都是看点他就是xxx. 13.它被誉为史上最牛悬疑片无数人在看完它时候一个月不敢照镜<EFBFBD><EFBFBD>这样一部仅适合部分年龄段观看的影片究竟有什么样的魅力竟然获得某瓣8.2的高分很多人说这部电影到处都是看点他就是xxx.
14.这是一部在某瓣上被70万人打出9.3分的高分的电影到底是一部什么样的电影能够在某瓣上被70万人打出9.3分的高分 14.这是一部在某瓣上被70万人打出9.3分的高分的电影到底是一部什么样的电影能够在某瓣上被70万人打出9.3分的高分
15.这是一部细思极恐的科幻大片整部电影颠覆你的三观它的名字叫 15.这是一部细思极恐的科幻大片整部电影颠覆你的三观它的名字叫
16.史上最震撼的灾难片每一点都不舍得快进的电影他叫 16.史上最震撼的灾难片每一点都不舍得快进的电影他叫
@ -66,7 +66,7 @@ Method = """
2.这是一部印度高分悬疑片 2.这是一部印度高分悬疑片
3.这部电影原在日本因为而被下架 3.这部电影原在日本因为而被下架
4.这是韩国最恐怖的犯罪片 4.这是韩国最恐怖的犯罪片
5.这是最近国产片评分最高的悬疑 5.这是最近国产片评分最高的悬疑<EFBFBD><EFBFBD>
以上均按照影片国家来区分然后简单介绍下主题就可以开始直接叙述作品也是一个很不错的方法 以上均按照影片国家来区分然后简单介绍下主题就可以开始直接叙述作品也是一个很不错的方法
### 方式四:如何自由发挥 ### 方式四:如何自由发挥
@ -97,7 +97,7 @@ Method = """
后面水平越来越高的时候可以进行人生道理的讲评 后面水平越来越高的时候可以进行人生道理的讲评
比如这部电影告诉我们 比如这部电影告诉我们
类似于哲理性质作为一个总结 类似于哲理性质<EFBFBD><EFBFBD>作为一个总结
也可以把最后的影视反转原生放出来留下悬念 也可以把最后的影视反转原生放出来留下悬念
比如也可以总结下这部短片如何的好推荐/值得大家去观看之类的话语 比如也可以总结下这部短片如何的好推荐/值得大家去观看之类的话语
@ -426,7 +426,7 @@ def compress_video(input_path: str, output_path: str):
def generate_script( def generate_script(
video_path: str, video_plot: str, video_name: str, language: str = "zh-CN", progress_text: st.empty = st.empty() video_path: str, video_plot: str, video_name: str, language: str = "zh-CN", progress_callback=None
) -> str: ) -> str:
""" """
生成视频剪辑脚本 生成视频剪辑脚本
@ -435,73 +435,102 @@ def generate_script(
video_plot: 视频剧情内容 video_plot: 视频剧情内容
video_name: 视频名称 video_name: 视频名称
language: 语言 language: 语言
progress_callback: 进度回调函数
Returns: Returns:
str: 生成的脚本 str: 生成的脚本
""" """
# 1. 压缩视频 # 1. 压缩视频
progress_text.text("压缩视频中...")
compressed_video_path = f"{os.path.splitext(video_path)[0]}_compressed.mp4" compressed_video_path = f"{os.path.splitext(video_path)[0]}_compressed.mp4"
compress_video(video_path, compressed_video_path) compress_video(video_path, compressed_video_path)
# # 2. 转录视频 # 在关键步骤更新进度
# transcription = gemini_video_transcription( if progress_callback:
# video_name=video_name, progress_callback(15, "压缩完成") # 例如,在压缩视频后
# video_path=compressed_video_path,
# language=language, # 2. 转录视频
# progress_text=progress_text, transcription = gemini_video_transcription(
# llm_provider_video="gemini" video_name=video_name,
# ) video_path=compressed_video_path,
transcription = """ language=language,
[{"timestamp": "00:00-00:06", "picture": "一个穿着蓝色囚服,戴着手铐的人在房间里走路。", "speech": ""}, llm_provider_video="gemini",
{"timestamp": "00:06-00:09", "picture": "一个穿着蓝色囚服,戴着手铐的人,画面上方显示“李自忠 银行抢劫犯”。", "speech": "李自忠 银行抢劫一案 现在宣判"}, progress_callback=progress_callback
{"timestamp": "00:09-00:12", "picture": "一个穿着黑色西装,打着红色领带的女人,坐在一个牌子上,牌子上写着“书记员”,身后墙上挂着“国徽”。", "speech": "全体起立"}, )
{"timestamp": "00:12-00:15", "picture": "一个穿着黑色法官服的男人坐在一个牌子后面,牌子上写着“审判长”,身后墙上挂着“国徽”。法庭上,很多人站着。", "speech": ""}, if progress_callback:
{"timestamp": "00:15-00:19", "picture": "一个穿着黑色西装,打着红色领带的女人,坐在一个牌子上,牌子上写着“书记员”,身后墙上挂着“国徽”。法庭上,很多人站着。", "speech": "本庭二审判决如下 被告李自忠 犯抢劫银行罪"}, progress_callback(60, "生成解说文案...") # 例如,在转录视频后
{"timestamp": "00:19-00:24", "picture": "一个穿着蓝色囚服,戴着手铐的人,画面上方显示“李自忠 银行抢劫犯”。", "speech": "维持一审判决 判处有期徒刑 二十年"},
{"timestamp": "00:24-00:27", "picture": "一个穿着黑色法官服的男人坐在一个牌子后面,牌子上写着“审判长”,他敲了一下法槌。", "speech": ""},
{"timestamp": "00:27-00:32", "picture": "一个穿着蓝色囚服,戴着手铐的人,画面上方显示“李自忠 银行抢劫犯”。", "speech": "我们要让她们牢底坐穿 越父啊越父 你一个平头老百姓 也敢跟外资银行做对 真是不知天高地厚"},
{"timestamp": "00:32-00:41", "picture": "一个穿着蓝色囚服,戴着手铐的人跪在地上。", "speech": "我要让她们牢底坐穿 越父啊越父 你一个平头老百姓 也敢跟外资银行做对 真是不知天高地厚"},
{"timestamp": "00:41-00:47", "picture": "两个警察押解着一个穿着蓝色囚服,戴着手铐的人走在路上,一个女记者在路边报道新闻。", "speech": "李先生 这里是孔雀卫视 这里是黄金眼819新闻直播间 这里是浙江卫视新闻直播间 近日李自忠案引发社会热议"},
{"timestamp": "00:47-01:03", "picture": "一个穿着灰色外套的男人坐在银行柜台前,和银行工作人员说话。画面中还穿插着女记者在路边报道新闻的画面。", "speech": "李自忠案引发社会热议 李自忠在去银行取钱的时候 由于他拿的是儿子的存折 所以银行要求李自忠证明他的儿子就是他的儿子 我说取不了就是取不了啊 这是你儿子的存折啊 你要证明你儿子是你儿子啊"},
{"timestamp": "01:03-01:10", "picture": "一个穿着灰色外套的男人坐在银行柜台前,和银行工作人员说话。画面中还穿插着女记者在路边报道新闻的画面。", "speech": "李自忠提供了身份证账户户口本后 银行都不认可他的儿子是他的儿子 就在这个时候 银行发生一起抢劫案"},
{"timestamp": "01:10-01:17", "picture": "三个戴着帽子和口罩的劫匪持枪闯入银行,银行里的人都很害怕,纷纷蹲下躲避。", "speech": "都给我蹲下 老实点 把钱给我交出来"},
{"timestamp": "01:17-01:28", "picture": "女记者在路边报道新闻,画面中穿插着银行抢劫案的画面。", "speech": "劫匪看到一旁大哭的李自忠 得知他是因为儿子需要治病才取钱的时候 给了他一打钱 怎么 你儿子在医院等着钱救命啊 银行不给取啊"},
{"timestamp": "01:28-01:36", "picture": "一个戴着黑色帽子和口罩的劫匪,拿着枪,给一个穿着灰色外套的男人一叠钱。", "speech": "银行不给取啊 好了 给儿子看病去 李自忠在把钱给儿子交完药费后被捕"},
{"timestamp": "01:36-01:58", "picture": "两个警察押解着一个穿着蓝色囚服,戴着手铐的男人走在路上,一个女记者在路边报道新闻。", "speech": "目前一审二审都维持原判 判处有期徒刑二十年 对此你有什么想说的吗 他怎么证明他儿子是他儿子 要是银行早点把钱给我 我也不会遇到劫匪 我儿子还得救命 不是的 儿子 儿子 儿子"},
{"timestamp": "01:58-02:03", "picture": "两个警察押解着一个穿着蓝色囚服,戴着手铐的男人走在路上,一个女记者在路边报道新闻。男人情绪激动,大声喊叫。", "speech": "儿子 儿子 儿子"},
{"timestamp": "02:03-02:12", "picture": "一个病房里,一个年轻男人躺在病床上,戴着呼吸机,一个穿着粉色上衣的女人站在病床边。画面中穿插着新闻报道的画面。", "speech": "近日李自忠案引发社会热议 李自忠在去银行取钱的时候 银行要求李自忠证明他的儿子就是他的儿子"},
{"timestamp": "02:12-02:25", "picture": "一个病房里,一个年轻男人躺在病床上,戴着呼吸机,一个穿着粉色上衣的女人站在病床边,一个白头发的医生站在门口。", "speech": "爸 这家人也真够可怜的 当爹的坐牢 这儿子 恐怕要成植物人了"},
{"timestamp": "02:25-02:31", "picture": "一个病房里,一个年轻男人躺在病床上,戴着呼吸机,一个穿着粉色上衣的女人站在病床边,一个白头发的医生站在门口。", "speech": "医生啊 我弟弟的情况怎么样 我先看看"},
{"timestamp": "02:31-02:40", "picture": "一个病房里,一个年轻男人躺在病床上,戴着呼吸机,一个穿着粉色上衣的女人站在病床边,一个白头发的医生正在给男人做检查。", "speech": ""},
{"timestamp": "02:40-02:46", "picture": "一个病房里,一个年轻男人躺在病床上,戴着呼吸机,一个穿着粉色上衣的女人站在病床边,一个白头发的医生正在给男人做检查。", "speech": "不太理想啊 你弟弟想要醒过来 希望渺茫"},
{"timestamp": "02:46-02:57", "picture": "一个病房里,一个年轻男人躺在病床上,戴着呼吸机,一个穿着粉色上衣的女人站在病床边,一个白头发的医生正在给男人做检查。", "speech": "这 麟木 麟木你别吓姐啊麟木 麟木"},
{"timestamp": "02:57-03:02", "picture": "一个病房里,一个年轻男人躺在病床上,戴着呼吸机,一个穿着粉色上衣的女人站在病床边,一个白头发的医生正在给男人做检查。画面中穿插着新闻报道的画面。", "speech": "麟木 儿子 麟木你别吓姐啊麟木"},
{"timestamp": "03:02-03:08", "picture": "一个病房里,一个年轻男人躺在病床上,戴着呼吸机,一个穿着粉色上衣的女人站在病床边,一个白头发的医生正在给男人做检查。画面中穿插着新闻报道的画面。女人情绪激动,大声哭泣。", "speech": "儿子 麟木你别吓姐啊麟木 儿子"},
{"timestamp": "03:08-03:14", "picture": "一个病房里,一个年轻男人躺在病床上,戴着呼吸机,一个穿着粉色上衣的女人站在病床边,一个白头发的医生正在给男人做检查。画面中穿插着新闻报道的画面。女人情绪激动,大声哭泣。", "speech": "儿子"},
{"timestamp": "03:14-03:18", "picture": "一个病房里,一个年轻男人躺在病床上,戴着呼吸机,画面变成紫色光效。", "speech": ""},
{"timestamp": "03:18-03:20", "picture": "一个病房里,一个年轻男人躺在病床上,戴着呼吸机,他突然睁开了眼睛。", "speech": ""}]
"""
# # 清理压缩后的视频文件
# try:
# os.remove(compressed_video_path)
# except OSError as e:
# logger.warning(f"删除压缩视频文件失败: {e}")
# 3. 编写解说文案 # 3. 编写解说文案
progress_text.text("解说文案中...")
script = writing_short_play(video_plot, video_name, "openai", count=300) script = writing_short_play(video_plot, video_name, "openai", count=300)
# 在关键步骤更新进度
if progress_callback:
progress_callback(70, "匹配画面...") # 例如,在生成脚本后
# 4. 文案匹配画面 # 4. 文案匹配画面
if transcription != "": if transcription != "":
progress_text.text("画面匹配中...")
matched_script = screen_matching(huamian=transcription, wenan=script, llm_provider="openai") matched_script = screen_matching(huamian=transcription, wenan=script, llm_provider="openai")
# 在关键步骤更新进度
if progress_callback:
progress_callback(80, "匹配成功")
return matched_script return matched_script
else: else:
return "" return ""
def gemini_video_transcription(video_name: str, video_path: str, language: str, llm_provider_video: str, progress_callback=None):
'''
使用 gemini-1.5-xxx 进行视频画面转录
'''
api_key = config.app.get("gemini_api_key")
gemini.configure(api_key=api_key)
prompt = """
请转录音频包括时间戳并提供视觉描述然后以 JSON 格式输出当前视频中使用的语言为 %s
在转录视频时请通过确保以下条件来完成转录
1. 画面描述使用语言: %s 进行输出
2. 同一个画面合并为一个转录记录
3. 使用以下 JSON schema:
Graphics = {"timestamp": "MM:SS-MM:SS"(时间戳格式), "picture": "str"(画面描述), "speech": "str"(台词如果没有人说话则使用空字符串)}
Return: list[Graphics]
4. 请以严格的 JSON 格式返回数据不要包含任何注释标记或其他字符数据应符合 JSON 语法可以被 json.loads() 函数直接解析 不要添加 ```json 或其他标记
""" % (language, language)
logger.debug(f"视频名称: {video_name}")
try:
if progress_callback:
progress_callback(20, "上传视频至 Google cloud")
gemini_video_file = gemini.upload_file(video_path)
logger.debug(f"视频 {gemini_video_file.name} 上传至 Google cloud 成功, 开始解析...")
while gemini_video_file.state.name == "PROCESSING":
gemini_video_file = gemini.get_file(gemini_video_file.name)
if progress_callback:
progress_callback(30, "上传成功, 开始解析") # 更新进度为20%
if gemini_video_file.state.name == "FAILED":
raise ValueError(gemini_video_file.state.name)
elif gemini_video_file.state.name == "ACTIVE":
if progress_callback:
progress_callback(40, "解析完成, 开始转录...") # 更新进度为30%
logger.debug("解析完成, 开始转录...")
except ResumableUploadError as err:
logger.error(f"上传视频至 Google cloud 失败, 用户的位置信息不支持用于该API; \n{traceback.format_exc()}")
return False
except FailedPrecondition as err:
logger.error(f"400 用户位置不支持 Google API 使用。\n{traceback.format_exc()}")
return False
if progress_callback:
progress_callback(50, "开始转录")
try:
response = _generate_response_video(prompt=prompt, llm_provider_video=llm_provider_video, video_file=gemini_video_file)
logger.success("视频转录成功")
logger.debug(response)
print(type(response))
return response
except Exception as err:
return handle_exception(err)
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]:
prompt = f""" prompt = f"""
# Role: Video Search Terms Generator # Role: Video Search Terms Generator
@ -652,56 +681,6 @@ def gemini_video2json(video_origin_name: str, video_origin_path: str, video_plot
return response return response
def gemini_video_transcription(video_name: str, video_path: str, language: str, llm_provider_video: str, progress_text: st.empty = ""):
'''
使用 gemini-1.5-xxx 进行视频画面转录
'''
api_key = config.app.get("gemini_api_key")
gemini.configure(api_key=api_key)
prompt = """
请转录音频包括时间戳并提供视觉描述然后以 JSON 格式输出当前视频中使用的语言为 %s
在转录视频时请通过确保以下条件来完成转录
1. 画面描述使用语言: %s 进行输出
2. 同一个画面合并为一个转录记录
3. 使用以下 JSON schema:
Graphics = {"timestamp": "MM:SS-MM:SS"(时间戳格式), "picture": "str"(画面描述), "speech": "str"(台词如果没有人说话则使用空字符串)}
Return: list[Graphics]
4. 请以严格的 JSON 格式返回数据不要包含任何注释标记或其他字符数据应符合 JSON 语法可以被 json.loads() 函数直接解析 不要添加 ```json 或其他标记
""" % (language, language)
logger.debug(f"视频名称: {video_name}")
try:
progress_text.text("上传视频中...")
gemini_video_file = gemini.upload_file(video_path)
logger.debug(f"视频 {gemini_video_file.name} 上传至 Google cloud 成功, 开始解析...")
while gemini_video_file.state.name == "PROCESSING":
gemini_video_file = gemini.get_file(gemini_video_file.name)
progress_text.text(f"解析视频中, 当前状态: {gemini_video_file.state.name}")
if gemini_video_file.state.name == "FAILED":
raise ValueError(gemini_video_file.state.name)
elif gemini_video_file.state.name == "ACTIVE":
progress_text.text("解析完成")
logger.debug("解析完成, 开始转录...")
except ResumableUploadError as err:
logger.error(f"上传视频至 Google cloud 失败, 用户的位置信息不支持用于该API; \n{traceback.format_exc()}")
return False
except FailedPrecondition as err:
logger.error(f"400 用户位置不支持 Google API 使用。\n{traceback.format_exc()}")
return False
progress_text.text("视频转录中...")
try:
response = _generate_response_video(prompt=prompt, llm_provider_video=llm_provider_video, video_file=gemini_video_file)
logger.success("视频转录成功")
logger.debug(response)
print(type(response))
return response
except Exception as err:
return handle_exception(err)
def writing_movie(video_plot, video_name, llm_provider): def writing_movie(video_plot, video_name, llm_provider):
""" """
影视解说电影解说 影视解说电影解说
@ -801,58 +780,58 @@ def screen_matching(huamian: str, wenan: str, llm_provider: str):
- 请以严格的 JSON 格式返回数据不要包含任何注释标记或其他字符数据应符合 JSON 语法可以被 json.loads() 函数直接解析 不要添加 ```json 或其他标记 - 请以严格的 JSON 格式返回数据不要包含任何注释标记或其他字符数据应符合 JSON 语法可以被 json.loads() 函数直接解析 不要添加 ```json 或其他标记
""" % (huamian, wenan) """ % (huamian, wenan)
prompt = """ # prompt = """
你是一位拥有10年丰富经验的影视解说创作专家你的任务是根据提供的视频转录脚本和解说文案创作一个引人入胜的解说脚本请按照以下要求完成任务 # 你是一位拥有10年丰富经验的影视解说创作专家。你的任务是根据提供的视频转录脚本和解说文案创作一个引人入胜的解说脚本。请按照以下要求完成任务
#
1. 输入数据 # 1. 输入数据
- 视频转录脚本包含时间戳画面描述和人物台词 # - 视频转录脚本:包含时间戳、画面描述和人物台词
- 解说文案需要你进行匹配和编排的内容 # - 解说文案:需要你进行匹配和编排的内容
- 视频转录脚本和文案 XML 标记<PICTURE></PICTURE> <COPYWRITER></COPYWRITER>分隔如下所示 # - 视频转录脚本和文案(由 XML 标记<PICTURE></PICTURE>和 <COPYWRITER></COPYWRITER>分隔)如下所示
视频转录脚本 # 视频转录脚本
<PICTURE> # <PICTURE>
%s # %s
</PICTURE> # </PICTURE>
文案 # 文案
<COPYWRITER> # <COPYWRITER>
%s # %s
</COPYWRITER> # </COPYWRITER>
#
2. 输出要求 # 2. 输出要求
- 格式严格的JSON格式可直接被json.loads()解析 # - 格式严格的JSON格式可直接被json.loads()解析
- 结构list[script]其中script为字典类型 # - 结构list[script]其中script为字典类型
- script字段 # - script字段
{ # {
"picture": "画面描述", # "picture": "画面描述",
"timestamp": "时间戳", # "timestamp": "时间戳",
"narration": "解说文案", # "narration": "解说文案",
"OST": true/false # "OST": true/false
} # }
#
3. 匹配规则 # 3. 匹配规则
a) 时间戳匹配 # a) 时间戳匹配:
- 根据文案内容选择最合适的画面时间段 # - 根据文案内容选择最合适的画面时间段
- 避免时间重叠确保画面不重复出现 # - 避免时间重叠,确保画面不重复出现
- 适当合并或删减片段不要完全照搬转录脚本 # - 适当合并或删减片段,不要完全照搬转录脚本
b) 画面描述与转录脚本保持一致 # b) 画面描述:与转录脚本保持一致
c) 解说文案 # c) 解说文案:
- 当OST为true时narration为空字符串 # - 当OST为true时narration为空字符串
- 当OST为false时narration为解说文案但是要确保文案字数不要超过 30若文案较长则添加到下一个片段 # - 当OST为false时narration为解说文案但是要确保文案字数不要超过 30字若文案较长则添加到下一个片段
d) OST原声 # d) OST原声
- 按1:1比例穿插原声和解说片段 # - 按1:1比例穿插原声和解说片段
- 第一个片段必须是原声时长不少于20秒 # - 第一个片段必须是原声时长不少于20秒
- 选择整个视频中最精彩的片段作为开场 # - 选择整个视频中最精彩的片段作为开场
#
4. 创作重点 # 4. 创作重点
- 确保解说与画面高度匹配 # - 确保解说与画面高度匹配
- 巧妙安排原声和解说的交替提升观众体验 # - 巧妙安排原声和解说的交替,提升观众体验
- 创造一个引人入胜节奏紧凑的解说脚本 # - 创造一个引人入胜、节奏紧凑的解说脚本
#
5. 注意事项 # 5. 注意事项
- 严格遵守JSON格式不包含任何注释或额外标记 # - 严格遵守JSON格式不包含任何注释或额外标记
- 充分利用你的专业经验创作出高质量吸引人的解说内容 # - 充分利用你的专业经验,创作出高质量、吸引人的解说内容
#
请基于以上要求将提供的视频转录脚本和解说文案整合成一个专业吸引人的解说脚本你的创作将直接影响观众的观看体验请发挥你的专业素养创作出最佳效果 # 请基于以上要求,将提供的视频转录脚本和解说文案整合成一个专业、吸引人的解说脚本。你的创作将直接影响观众的观看体验,请发挥你的专业素养,创作出最佳效果。
""" % (huamian, wenan) # """ % (huamian, wenan)
try: try:
response = _generate_response(prompt, llm_provider) response = _generate_response(prompt, llm_provider)
logger.success("匹配成功") logger.success("匹配成功")

View File

@ -267,7 +267,6 @@ def save_clip_video(timestamp: str, origin_video: str, save_dir: str = "") -> di
if not os.path.exists(save_dir): if not os.path.exists(save_dir):
os.makedirs(save_dir) os.makedirs(save_dir)
# url_hash = utils.md5(str(uuid.uuid4()))
video_id = f"vid-{timestamp.replace(':', '_')}" video_id = f"vid-{timestamp.replace(':', '_')}"
video_path = f"{save_dir}/{video_id}.mp4" video_path = f"{save_dir}/{video_id}.mp4"
@ -278,7 +277,7 @@ def save_clip_video(timestamp: str, origin_video: str, save_dir: str = "") -> di
# 剪辑视频 # 剪辑视频
start, end = utils.split_timestamp(timestamp) start, end = utils.split_timestamp(timestamp)
video = VideoFileClip(origin_video).subclip(start, end) video = VideoFileClip(origin_video).subclip(start, end)
video.write_videofile(video_path) video.write_videofile(video_path, logger=None) # 禁用 MoviePy 的内置日志
if os.path.getsize(video_path) > 0 and os.path.exists(video_path): if os.path.getsize(video_path) > 0 and os.path.exists(video_path):
try: try:
@ -297,20 +296,21 @@ def save_clip_video(timestamp: str, origin_video: str, save_dir: str = "") -> di
return {} return {}
def clip_videos(task_id: str, timestamp_terms: List[str], origin_video: str, ) -> dict: def clip_videos(task_id: str, timestamp_terms: List[str], origin_video: str, progress_callback=None):
""" """
剪辑视频 剪辑视频
Args: Args:
task_id: 任务id task_id: 任务id
timestamp_terms: 需要剪辑的时间戳列表:['00:00-00:20', '00:36-00:40', '07:07-07:22'] timestamp_terms: 需要剪辑的时间戳列表:['00:00-00:20', '00:36-00:40', '07:07-07:22']
origin_video: 原视频路径 origin_video: 原视频路径
progress_callback: 进度回调函数
Returns: Returns:
剪辑后的视频路径 剪辑后的视频路径
""" """
video_paths = {} video_paths = {}
for item in timestamp_terms: total_items = len(timestamp_terms)
logger.info(f"需要裁剪 '{origin_video}'{len(timestamp_terms)} 个视频") for index, item in enumerate(timestamp_terms):
material_directory = config.app.get("material_directory", "").strip() material_directory = config.app.get("material_directory", "").strip()
if material_directory == "task": if material_directory == "task":
material_directory = utils.task_dir(task_id) material_directory = utils.task_dir(task_id)
@ -318,11 +318,14 @@ def clip_videos(task_id: str, timestamp_terms: List[str], origin_video: str, ) -
material_directory = "" material_directory = ""
try: try:
logger.info(f"clip video: {item}")
saved_video_path = save_clip_video(timestamp=item, origin_video=origin_video, save_dir=material_directory) saved_video_path = save_clip_video(timestamp=item, origin_video=origin_video, save_dir=material_directory)
if saved_video_path: if saved_video_path:
logger.info(f"video saved: {saved_video_path}") logger.info(f"video saved: {saved_video_path}")
video_paths.update(saved_video_path) video_paths.update(saved_video_path)
# 更新进度
if progress_callback:
progress_callback(index + 1, total_items)
except Exception as e: except Exception as e:
logger.error(f"视频裁剪失败: {utils.to_json(item)} => {str(e)}") logger.error(f"视频裁剪失败: {utils.to_json(item)} => {str(e)}")
return {} return {}

View File

@ -48,7 +48,10 @@ def create(audio_file, subtitle_file: str = ""):
) )
try: try:
model = WhisperModel( model = WhisperModel(
model_size_or_path=model_path, device=device, compute_type=compute_type, local_files_only=True model_size_or_path=model_path,
device=device,
compute_type=compute_type,
local_files_only=True
) )
except Exception as e: except Exception as e:
logger.error( logger.error(
@ -72,6 +75,7 @@ def create(audio_file, subtitle_file: str = ""):
word_timestamps=True, word_timestamps=True,
vad_filter=True, vad_filter=True,
vad_parameters=dict(min_silence_duration_ms=500), vad_parameters=dict(min_silence_duration_ms=500),
initial_prompt="以下是普通话的句子"
) )
logger.info( logger.info(

View File

@ -2,6 +2,7 @@ import math
import json import json
import os.path import os.path
import re import re
import traceback
from os import path from os import path
from loguru import logger from loguru import logger
@ -323,7 +324,7 @@ def start(task_id, params: VideoParams, stop_at: str = "video"):
return kwargs return kwargs
def start_subclip(task_id: str, params: VideoClipParams, subclip_path_videos): def start_subclip(task_id: str, params: VideoClipParams, subclip_path_videos: list):
""" """
后台任务自动剪辑视频进行剪辑 后台任务自动剪辑视频进行剪辑
@ -340,6 +341,7 @@ def start_subclip(task_id: str, params: VideoClipParams, subclip_path_videos):
logger.info("\n\n## 1. 加载视频脚本") logger.info("\n\n## 1. 加载视频脚本")
video_script_path = path.join(params.video_clip_json_path) video_script_path = path.join(params.video_clip_json_path)
# video_script_path = video_clip_json_path
# 判断json文件是否存在 # 判断json文件是否存在
if path.exists(video_script_path): if path.exists(video_script_path):
try: try:
@ -361,6 +363,7 @@ def start_subclip(task_id: str, params: VideoClipParams, subclip_path_videos):
logger.error(f"无法读取视频json脚本请检查配置是否正确。{e}") logger.error(f"无法读取视频json脚本请检查配置是否正确。{e}")
raise ValueError("无法读取视频json脚本请检查配置是否正确") raise ValueError("无法读取视频json脚本请检查配置是否正确")
else: else:
logger.error(f"video_script_path: {video_script_path} \n\n", traceback.format_exc())
raise ValueError("解说脚本不存在!请检查配置是否正确。") raise ValueError("解说脚本不存在!请检查配置是否正确。")
logger.info("\n\n## 2. 生成音频列表") logger.info("\n\n## 2. 生成音频列表")

View File

@ -1,9 +1,12 @@
import locale import locale
import os import os
import traceback
import requests import requests
import threading import threading
from typing import Any from typing import Any
from loguru import logger from loguru import logger
import streamlit as st
import json import json
from uuid import uuid4 from uuid import uuid4
import urllib3 import urllib3
@ -11,6 +14,7 @@ from datetime import datetime, timedelta
from app.models import const from app.models import const
from app.utils import check_script from app.utils import check_script
from app.services import material
urllib3.disable_warnings() urllib3.disable_warnings()
@ -372,10 +376,52 @@ def add_new_timestamps(scenes):
def clean_model_output(output): def clean_model_output(output):
""" # 移除可能的代码块标记
模型输出包含 ```json 标记时的处理 output = output.strip('```json').strip('```')
""" # 移除开头和结尾的空白字符
if "```json" in output: output = output.strip()
print("##########") return output
output = output.replace("```json", "").replace("```", "")
return output.strip()
def cut_video(params, progress_callback=None):
try:
task_id = str(uuid4())
st.session_state['task_id'] = task_id
if not st.session_state.get('video_clip_json'):
raise ValueError("视频脚本不能为空")
video_script_list = st.session_state['video_clip_json']
time_list = [i['timestamp'] for i in video_script_list]
total_clips = len(time_list)
def clip_progress(current, total):
progress = int((current / total) * 100)
if progress_callback:
progress_callback(progress)
subclip_videos = material.clip_videos(
task_id=task_id,
timestamp_terms=time_list,
origin_video=params.video_origin_path,
progress_callback=clip_progress
)
if subclip_videos is None:
raise ValueError("裁剪视频失败")
st.session_state['subclip_videos'] = subclip_videos
for i, video_script in enumerate(video_script_list):
try:
video_script['path'] = subclip_videos[video_script['timestamp']]
except KeyError as err:
logger.error(f"裁剪视频失败: {err}")
raise ValueError(f"裁剪视频失败: {err}")
return task_id, subclip_videos
except Exception as e:
logger.error(f"视频裁剪过程中发生错误: {traceback.format_exc()}")
raise

186
webui.py
View File

@ -61,13 +61,11 @@ config_file = os.path.join(root_dir, "webui", ".streamlit", "webui.toml")
system_locale = utils.get_system_locale() system_locale = utils.get_system_locale()
if 'video_clip_json' not in st.session_state: if 'video_clip_json' not in st.session_state:
st.session_state['video_clip_json'] = '' st.session_state['video_clip_json'] = []
if 'video_plot' not in st.session_state: if 'video_plot' not in st.session_state:
st.session_state['video_plot'] = '' st.session_state['video_plot'] = ''
if 'ui_language' not in st.session_state: if 'ui_language' not in st.session_state:
st.session_state['ui_language'] = config.ui.get("language", system_locale) st.session_state['ui_language'] = config.ui.get("language", system_locale)
if 'script_generation_status' not in st.session_state:
st.session_state['script_generation_status'] = ""
def get_all_fonts(): def get_all_fonts():
@ -124,7 +122,7 @@ def init_log():
_lvl = "DEBUG" _lvl = "DEBUG"
def format_record(record): def format_record(record):
# 获取日志记录中的文件全 # 获取日志记录中的文件全<EFBFBD><EFBFBD><EFBFBD>
file_path = record["file"].path file_path = record["file"].path
# 将绝对路径转换为相对于项目根目录的路径 # 将绝对路径转换为相对于项目根目录的路径
relative_path = os.path.relpath(file_path, root_dir) relative_path = os.path.relpath(file_path, root_dir)
@ -272,7 +270,7 @@ with left_panel:
# 按创建时间降序排序 # 按创建时间降序排序
script_list.sort(key=lambda x: x["ctime"], reverse=True) script_list.sort(key=lambda x: x["ctime"], reverse=True)
# 本文件 下拉框 # <EFBFBD><EFBFBD>本文件 下拉框
script_path = [(tr("Auto Generate"), ""), ] script_path = [(tr("Auto Generate"), ""), ]
for file in script_list: for file in script_list:
display_name = file['file'].replace(root_dir, "") display_name = file['file'].replace(root_dir, "")
@ -282,8 +280,9 @@ with left_panel:
options=range(len(script_path)), # 使用索引作为内部选项值 options=range(len(script_path)), # 使用索引作为内部选项值
format_func=lambda x: script_path[x][0] # 显示给用户的是标签 format_func=lambda x: script_path[x][0] # 显示给用户的是标签
) )
params.video_clip_json = script_path[selected_script_index][1] params.video_clip_json_path = script_path[selected_script_index][1]
video_json_file = params.video_clip_json config.app["video_clip_json_path"] = params.video_clip_json_path
st.session_state['video_clip_json_path'] = params.video_clip_json_path
# 视频文件处理 # 视频文件处理
video_files = [] video_files = []
@ -301,18 +300,20 @@ with left_panel:
}) })
# 按创建时间降序排序 # 按创建时间降序排序
video_list.sort(key=lambda x: x["ctime"], reverse=True) video_list.sort(key=lambda x: x["ctime"], reverse=True)
video_path = [("None", ""), (tr("Upload Local Files"), "local")] video_path = [(tr("None"), ""), (tr("Upload Local Files"), "local")]
for code in [file['file'] for file in video_list]: for file in video_list:
video_path.append((code, code)) display_name = file['file'].replace(root_dir, "")
video_path.append((display_name, file['file']))
# 视频文件 # 视频文件
selected_video_index = st.selectbox(tr("Video File"), selected_video_index = st.selectbox(tr("Video File"),
index=0, index=0,
options=range(len(video_path)), # 使用索引作为内部选项值 options=range(len(video_path)), # 使用索引作为内部选项值
format_func=lambda x: video_path[x][0] # 显示给用户的是标 format_func=lambda x: video_path[x][0] # 显示给用户的是标
) )
params.video_origin_path = video_path[selected_video_index][1] params.video_origin_path = video_path[selected_video_index][1]
config.app["video_origin_path"] = params.video_origin_path config.app["video_origin_path"] = params.video_origin_path
st.session_state['video_origin_path'] = params.video_origin_path
# 从本地上传 mp4 文件 # 从本地上传 mp4 文件
if params.video_origin_path == "local": if params.video_origin_path == "local":
@ -347,40 +348,73 @@ with left_panel:
) )
# 生成视频脚本 # 生成视频脚本
st.session_state['script_generation_status'] = "开始生成视频脚本" if st.session_state['video_clip_json_path']:
if st.button(tr("Video Script Generate"), key="auto_generate_script"): generate_button_name = tr("Video Script Load")
with st.spinner("正在生成脚本..."): else:
# 这里可以用 st.empty() 来动态更新文本 generate_button_name = tr("Video Script Generate")
progress_text = st.empty() if st.button(generate_button_name, key="auto_generate_script"):
progress_text.text("正在处理...") progress_bar = st.progress(0)
status_text = st.empty()
if video_json_file == "" and params.video_origin_path != "": def update_progress(progress: float, message: str = ""):
progress_text.text("开始压缩...") progress_bar.progress(progress)
if message:
status_text.text(f"{progress}% - {message}")
else:
status_text.text(f"进度: {progress}%")
try:
with st.spinner("正在生成脚本..."):
if not video_name:
st.warning("视频名称不能为空")
st.stop()
if not video_plot:
st.warning("视频剧情不能为空")
st.stop()
if params.video_clip_json_path == "" and params.video_origin_path != "":
update_progress(10, "压缩视频中...")
# 使用大模型生成视频脚本 # 使用大模型生成视频脚本
script = llm.generate_script( script = llm.generate_script(
video_path=params.video_origin_path, video_path=params.video_origin_path,
video_plot=video_plot, video_plot=video_plot,
video_name=video_name, video_name=video_name,
language=params.video_language, language=params.video_language,
progress_text=progress_text progress_callback=update_progress
) )
if script is None: if script is None:
st.error("生成脚本失败,请检查日志") st.error("生成脚本失败,请检查日志")
st.stop() st.stop()
st.session_state['video_clip_json'] = script
cleaned_string = script.strip("```json").strip("```")
st.session_state['video_script_list'] = json.loads(cleaned_string)
else: else:
with open(video_json_file, 'r', encoding='utf-8') as f: update_progress(90)
script = utils.clean_model_output(script)
st.session_state['video_clip_json'] = json.loads(script)
else:
# 从本地加载
with open(params.video_clip_json_path, 'r', encoding='utf-8') as f:
update_progress(50)
status_text.text("从本地加载中...")
script = f.read() script = f.read()
st.session_state['video_clip_json'] = script script = utils.clean_model_output(script)
cleaned_string = script.strip("```json").strip("```") st.session_state['video_clip_json'] = json.loads(script)
st.session_state['video_script_list'] = json.loads(cleaned_string) update_progress(100)
status_text.text("从本地加载成功")
time.sleep(0.5) # 给进度条一点时间到达100%
progress_bar.progress(100)
status_text.text("脚本生成完成!")
st.success("视频脚本生成成功!")
except Exception as e:
st.error(f"生成过程中发生错误: {traceback.format_exc()}")
finally:
time.sleep(2) # 给用户一些时间查看最终状态
progress_bar.empty()
status_text.empty()
# 视频脚本 # 视频脚本
video_clip_json_details = st.text_area( video_clip_json_details = st.text_area(
tr("Video Script"), tr("Video Script"),
value=st.session_state['video_clip_json'], value=json.dumps(st.session_state.video_clip_json, indent=2, ensure_ascii=False),
height=180 height=180
) )
@ -398,73 +432,43 @@ with left_panel:
timestamp = datetime.datetime.now().strftime("%Y-%m%d-%H%M%S") timestamp = datetime.datetime.now().strftime("%Y-%m%d-%H%M%S")
save_path = os.path.join(script_dir, f"{timestamp}.json") save_path = os.path.join(script_dir, f"{timestamp}.json")
# 尝试解析输入的 JSON 数据
input_json = str(video_clip_json_details)
# 去掉json的头尾标识
input_json = input_json.strip('```json').strip('```')
try: try:
data = utils.add_new_timestamps(json.loads(input_json)) data = utils.add_new_timestamps(json.loads(video_clip_json_details))
except Exception as err: except Exception as err:
st.error(f"视频脚本格式错误,请检查脚本是否符合 JSON 格式;{err} \n\n{traceback.format_exc()}") st.error(f"视频脚本格式错误,请检查脚本是否符合 JSON 格式;{err} \n\n{traceback.format_exc()}")
st.stop() st.stop()
# 检查是否是一个列表
if not isinstance(data, list):
st.error("JSON is not a list")
st.stop()
# 检查列表中的每个元素是否包含所需的键
required_keys = {"picture", "timestamp", "narration"}
for item in data:
if not isinstance(item, dict):
st.error("List 元素不是字典")
st.stop()
if not required_keys.issubset(item.keys()):
st.error("Dict 元素不包含必需的键")
st.stop()
# 存储为新的 JSON 文件 # 存储为新的 JSON 文件
with open(save_path, 'w', encoding='utf-8') as file: with open(save_path, 'w', encoding='utf-8') as file:
json.dump(data, file, ensure_ascii=False, indent=4) json.dump(data, file, ensure_ascii=False, indent=4)
# 将data的值存储到 session_state 中,类似缓存 # 将data的值存储到 session_state 中,类似缓存
st.session_state['video_script_list'] = data st.session_state['video_clip_json'] = data
st.session_state['video_clip_json_path'] = save_path st.session_state['video_clip_json_path'] = save_path
# 刷新页面 # 刷新页面
st.rerun() # st.rerun()
def caijian():
with st.spinner(tr("裁剪视频中...")):
st.session_state['task_id'] = str(uuid4())
if st.session_state.get('video_script_list', None) is not None:
video_script_list = st.session_state.video_script_list
print(video_script_list)
print(type(video_script_list))
time_list = [i['timestamp'] for i in video_script_list]
subclip_videos = material.clip_videos(
task_id=st.session_state['task_id'],
timestamp_terms=time_list,
origin_video=params.video_origin_path
)
if subclip_videos is None:
st.error(tr("裁剪视频失败"))
st.stop()
st.session_state['subclip_videos'] = subclip_videos
for video_script in video_script_list:
try:
video_script['path'] = subclip_videos[video_script['timestamp']]
except KeyError as err:
st.error(f"裁剪视频失败 {err}")
logger.debug(f"当前的脚本为:{st.session_state.subclip_videos}")
else:
st.error(tr("请先生成视频脚本"))
# 裁剪视频 # 裁剪视频
with button_columns[1]: with button_columns[1]:
if st.button(tr("Crop Video"), key="auto_crop_video", use_container_width=True): if st.button(tr("Crop Video"), key="auto_crop_video", use_container_width=True):
caijian() progress_bar = st.progress(0)
status_text = st.empty()
def update_progress(progress):
progress_bar.progress(progress)
status_text.text(f"剪辑进度: {progress}%")
try:
utils.cut_video(params, update_progress)
time.sleep(0.5) # 给进度条一点时间到达100%
progress_bar.progress(100)
status_text.text("剪辑完成!")
st.success("视频剪辑成功完成!")
except Exception as e:
st.error(f"剪辑过程中发生错误: {str(e)}")
finally:
time.sleep(2) # 给用户一些时间查看最终状态
progress_bar.empty()
status_text.empty()
# 新中间面板 # 新中间面板
with middle_panel: with middle_panel:
@ -703,14 +707,16 @@ with st.expander(tr("Video Check"), expanded=False):
# 可编辑的输入框 # 可编辑的输入框
text_panels = st.columns(2) text_panels = st.columns(2)
with text_panels[0]: with text_panels[0]:
text1 = st.text_area(tr("timestamp"), value=initial_timestamp, height=20) text1 = st.text_area(tr("timestamp"), value=initial_timestamp, height=20,
key=f"timestamp_{index}")
with text_panels[1]: with text_panels[1]:
text2 = st.text_area(tr("Picture description"), value=initial_picture, height=20) text2 = st.text_area(tr("Picture description"), value=initial_picture, height=20,
logger.debug(initial_narration) key=f"picture_{index}")
text3 = st.text_area(tr("Narration"), value=initial_narration, height=100) text3 = st.text_area(tr("Narration"), value=initial_narration, height=100,
key=f"narration_{index}")
# 重新生成按钮 # 重新生成按钮
if st.button(tr("Rebuild"), key=f"button_{index}"): if st.button(tr("Rebuild"), key=f"rebuild_{index}"):
# 更新video_list中的对应项 # 更新video_list中的对应项
video_list[index]['timestamp'] = text1 video_list[index]['timestamp'] = text1
video_list[index]['picture'] = text2 video_list[index]['picture'] = text2
@ -722,9 +728,9 @@ with st.expander(tr("Video Check"), expanded=False):
# 更新session_state以确保更改被保存 # 更新session_state以确保更改被保存
st.session_state['video_clip_json'] = utils.to_json(video_list) st.session_state['video_clip_json'] = utils.to_json(video_list)
# 替换原JSON 文件 # 替换原JSON 文件
with open(video_json_file, 'w', encoding='utf-8') as file: with open(params.video_clip_json_path, 'w', encoding='utf-8') as file:
json.dump(video_list, file, ensure_ascii=False, indent=4) json.dump(video_list, file, ensure_ascii=False, indent=4)
caijian() utils.cut_video(params, progress_callback=None)
st.rerun() st.rerun()
# 开始按钮 # 开始按钮
@ -735,13 +741,15 @@ if start_button:
if st.session_state.get('video_script_json_path') is not None: if st.session_state.get('video_script_json_path') is not None:
params.video_clip_json = st.session_state.get('video_clip_json') params.video_clip_json = st.session_state.get('video_clip_json')
logger.debug(f"当前的脚本为:{params.video_clip_json}") logger.debug(f"当前的脚本文件为:{st.session_state.video_clip_json_path}")
logger.debug(f"当前的视频文件为:{st.session_state.video_origin_path}")
logger.debug(f"裁剪后是视频列表:{st.session_state.subclip_videos}")
if not task_id: if not task_id:
st.error(tr("请先裁剪视频")) st.error(tr("请先裁剪视频"))
scroll_to_bottom() scroll_to_bottom()
st.stop() st.stop()
if not params.video_clip_json: if not params.video_clip_json_path:
st.error(tr("脚本文件不能为空")) st.error(tr("脚本文件不能为空"))
scroll_to_bottom() scroll_to_bottom()
st.stop() st.stop()

View File

@ -10,7 +10,7 @@
"Auto Detect": "自动检测", "Auto Detect": "自动检测",
"Auto Generate": "自动生成", "Auto Generate": "自动生成",
"Video Name": "视频名称", "Video Name": "视频名称",
"Video Script": "视频脚本(:blue[①可不填使用AI生成 ②合理使用标点断句,有助于生成字幕]", "Video Script": "视频脚本(:blue[①使用AI生成 ②从本机加载]",
"Save Script": "保存脚本", "Save Script": "保存脚本",
"Crop Video": "裁剪视频", "Crop Video": "裁剪视频",
"Video File": "视频文件(:blue[1⃣支持上传视频文件(限制2G) 2⃣大文件建议直接导入 ./resource/videos 目录]", "Video File": "视频文件(:blue[1⃣支持上传视频文件(限制2G) 2⃣大文件建议直接导入 ./resource/videos 目录]",
@ -91,6 +91,7 @@
"timestamp": "时间戳", "timestamp": "时间戳",
"Picture description": "图片描述", "Picture description": "图片描述",
"Narration": "视频文案", "Narration": "视频文案",
"Rebuild": "重新生成" "Rebuild": "重新生成",
"Video Script Load": "加载视频脚本"
} }
} }