mirror of
https://github.com/linyqh/NarratoAI.git
synced 2025-12-11 18:42:49 +00:00
完成优化webui体验-剪辑逻辑进度90%;
待优化点: 1. 优化脚本-解说质量
This commit is contained in:
parent
dc4ce80ea5
commit
decac3b11d
@ -31,7 +31,7 @@ Method = """
|
||||
文案的前三句,是整部电影的概括总结,2-3句介绍后,开始叙述故事剧情!
|
||||
推荐新手(新号)做:(盘点型)
|
||||
盘点全球最恐怖的10部电影
|
||||
盘点全球最科幻的10部电影
|
||||
盘<EFBFBD><EFBFBD><EFBFBD>全球最科幻的10部电影
|
||||
盘点全球最悲惨的10部电影
|
||||
盘全球最值得看的10部灾难电影
|
||||
盘点全球最值得看的10部励志电影
|
||||
@ -43,13 +43,13 @@ Method = """
|
||||
4.是什么样的一个人被豆瓣网友称之为史上最牛P的老太太,都70岁了还要去贩毒……
|
||||
5.他是M国历史上最NB/惨/猖狂/冤枉……的囚犯/抢劫犯/……
|
||||
6.这到底是一部什么样的影片,他一个人就拿了4个顶级奖项,第一季8.7分,第二季直接干到9.5分,11万人给出5星好评,一共也就6集,却斩获26项国际大奖,看过的人都说,他是近年来最好的xxx剧,几乎成为了近年来xxx剧的标杆。故事发生在……
|
||||
7.他是国产电影的巅峰佳作,更是许多80-90后的青春启蒙,曾入选《时代》周刊,获得年度佳片第一,可在国内却被尘封多年,至今为止都无法在各大视频网站看到完整资源,他就是《xxxxxx》
|
||||
7.他是国产电影的巅峰佳作,更是许多80-90后的青春启蒙,曾入选《<EFBFBD><EFBFBD>代》周刊,获得年度佳片第一,可在国内却被尘封多年,至今为止都无法在各大视频网站看到完整资源,他就是《xxxxxx》
|
||||
8.这是一部让所有人看得荷尔蒙飙升的爽片……
|
||||
9.他被成为世界上最虐心绝望的电影,至今无人敢看第二遍,很难想象,他是根据真实事件改编而来……
|
||||
10.这大概是有史以来最令人不寒而栗的电影,当年一经放映,就点燃了无数人的怒火,不少观众不等影片放完,就愤然离场,它比《xxx》更让人绝望,比比《xxx》更让人xxx,能坚持看完全片的人,更是万中无一,包括我。甚至观影结束后,有无数人抵制投诉这部电影,认为影片的导演玩弄了他们的情感!他是顶级神作《xxxx》……
|
||||
11.这是X国有史以来最高赞的一部悬疑电影,然而却因为某些原因,国内90%的人,没能看过这部片子,他就是《xxx》……
|
||||
12.有这样一部电影,这辈子,你绝对不想再看第二遍,并不是它剧情烂俗,而是它的结局你根本承受不起/想象不到……甚至有80%的观众在观影途中情绪崩溃中途离场,更让许多同行都不想解说这部电影,他就是大名鼎鼎的暗黑神作《xxx》…
|
||||
13.它被誉为史上最牛悬疑片无数人在看完它时候,一个月不敢照镜子,这样一部仅适合部分年龄段观看的影片,究竟有什么样的魅力,竟然获得某瓣8.2的高分,很多人说这部电影到处都是看点,他就是《xxx》….
|
||||
13.它被誉为史上最牛悬疑片无数人在看完它时候,一个月不敢照镜<EFBFBD><EFBFBD>,这样一部仅适合部分年龄段观看的影片,究竟有什么样的魅力,竟然获得某瓣8.2的高分,很多人说这部电影到处都是看点,他就是《xxx》….
|
||||
14.这是一部在某瓣上被70万人打出9.3分的高分的电影……到底是一部什么样的电影,能够在某瓣上被70万人打出9.3分的高分……
|
||||
15.这是一部细思极恐的科幻大片,整部电影颠覆你的三观,它的名字叫……
|
||||
16.史上最震撼的灾难片,每一点都不舍得快进的电影,他叫……
|
||||
@ -66,7 +66,7 @@ Method = """
|
||||
2.这是一部印度高分悬疑片,
|
||||
3.这部电影原在日本因为……而被下架,
|
||||
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(
|
||||
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:
|
||||
"""
|
||||
生成视频剪辑脚本
|
||||
@ -435,73 +435,102 @@ def generate_script(
|
||||
video_plot: 视频剧情内容
|
||||
video_name: 视频名称
|
||||
language: 语言
|
||||
progress_callback: 进度回调函数
|
||||
|
||||
Returns:
|
||||
str: 生成的脚本
|
||||
"""
|
||||
# 1. 压缩视频
|
||||
progress_text.text("压缩视频中...")
|
||||
compressed_video_path = f"{os.path.splitext(video_path)[0]}_compressed.mp4"
|
||||
compress_video(video_path, compressed_video_path)
|
||||
|
||||
# # 2. 转录视频
|
||||
# transcription = gemini_video_transcription(
|
||||
# video_name=video_name,
|
||||
# video_path=compressed_video_path,
|
||||
# language=language,
|
||||
# progress_text=progress_text,
|
||||
# llm_provider_video="gemini"
|
||||
# )
|
||||
transcription = """
|
||||
[{"timestamp": "00:00-00:06", "picture": "一个穿着蓝色囚服,戴着手铐的人在房间里走路。", "speech": ""},
|
||||
{"timestamp": "00:06-00:09", "picture": "一个穿着蓝色囚服,戴着手铐的人,画面上方显示“李自忠 银行抢劫犯”。", "speech": "李自忠 银行抢劫一案 现在宣判"},
|
||||
{"timestamp": "00:09-00:12", "picture": "一个穿着黑色西装,打着红色领带的女人,坐在一个牌子上,牌子上写着“书记员”,身后墙上挂着“国徽”。", "speech": "全体起立"},
|
||||
{"timestamp": "00:12-00:15", "picture": "一个穿着黑色法官服的男人坐在一个牌子后面,牌子上写着“审判长”,身后墙上挂着“国徽”。法庭上,很多人站着。", "speech": ""},
|
||||
{"timestamp": "00:15-00:19", "picture": "一个穿着黑色西装,打着红色领带的女人,坐在一个牌子上,牌子上写着“书记员”,身后墙上挂着“国徽”。法庭上,很多人站着。", "speech": "本庭二审判决如下 被告李自忠 犯抢劫银行罪"},
|
||||
{"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}")
|
||||
# 在关键步骤更新进度
|
||||
if progress_callback:
|
||||
progress_callback(15, "压缩完成") # 例如,在压缩视频后
|
||||
|
||||
# 2. 转录视频
|
||||
transcription = gemini_video_transcription(
|
||||
video_name=video_name,
|
||||
video_path=compressed_video_path,
|
||||
language=language,
|
||||
llm_provider_video="gemini",
|
||||
progress_callback=progress_callback
|
||||
)
|
||||
if progress_callback:
|
||||
progress_callback(60, "生成解说文案...") # 例如,在转录视频后
|
||||
|
||||
# 3. 编写解说文案
|
||||
progress_text.text("解说文案中...")
|
||||
script = writing_short_play(video_plot, video_name, "openai", count=300)
|
||||
|
||||
# 在关键步骤更新进度
|
||||
if progress_callback:
|
||||
progress_callback(70, "匹配画面...") # 例如,在生成脚本后
|
||||
|
||||
# 4. 文案匹配画面
|
||||
if transcription != "":
|
||||
progress_text.text("画面匹配中...")
|
||||
matched_script = screen_matching(huamian=transcription, wenan=script, llm_provider="openai")
|
||||
|
||||
# 在关键步骤更新进度
|
||||
if progress_callback:
|
||||
progress_callback(80, "匹配成功")
|
||||
return matched_script
|
||||
else:
|
||||
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]:
|
||||
prompt = f"""
|
||||
# Role: Video Search Terms Generator
|
||||
@ -652,56 +681,6 @@ def gemini_video2json(video_origin_name: str, video_origin_path: str, video_plot
|
||||
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):
|
||||
"""
|
||||
影视解说(电影解说)
|
||||
@ -801,58 +780,58 @@ def screen_matching(huamian: str, wenan: str, llm_provider: str):
|
||||
- 请以严格的 JSON 格式返回数据,不要包含任何注释、标记或其他字符。数据应符合 JSON 语法,可以被 json.loads() 函数直接解析, 不要添加 ```json 或其他标记。
|
||||
""" % (huamian, wenan)
|
||||
|
||||
prompt = """
|
||||
你是一位拥有10年丰富经验的影视解说创作专家。你的任务是根据提供的视频转录脚本和解说文案,创作一个引人入胜的解说脚本。请按照以下要求完成任务:
|
||||
|
||||
1. 输入数据:
|
||||
- 视频转录脚本:包含时间戳、画面描述和人物台词
|
||||
- 解说文案:需要你进行匹配和编排的内容
|
||||
- 视频转录脚本和文案(由 XML 标记<PICTURE></PICTURE>和 <COPYWRITER></COPYWRITER>分隔)如下所示:
|
||||
视频转录脚本
|
||||
<PICTURE>
|
||||
%s
|
||||
</PICTURE>
|
||||
文案:
|
||||
<COPYWRITER>
|
||||
%s
|
||||
</COPYWRITER>
|
||||
|
||||
2. 输出要求:
|
||||
- 格式:严格的JSON格式,可直接被json.loads()解析
|
||||
- 结构:list[script],其中script为字典类型
|
||||
- script字段:
|
||||
{
|
||||
"picture": "画面描述",
|
||||
"timestamp": "时间戳",
|
||||
"narration": "解说文案",
|
||||
"OST": true/false
|
||||
}
|
||||
|
||||
3. 匹配规则:
|
||||
a) 时间戳匹配:
|
||||
- 根据文案内容选择最合适的画面时间段
|
||||
- 避免时间重叠,确保画面不重复出现
|
||||
- 适当合并或删减片段,不要完全照搬转录脚本
|
||||
b) 画面描述:与转录脚本保持一致
|
||||
c) 解说文案:
|
||||
- 当OST为true时,narration为空字符串
|
||||
- 当OST为false时,narration为解说文案,但是要确保文案字数不要超过 30字,若文案较长,则添加到下一个片段
|
||||
d) OST(原声):
|
||||
- 按1:1比例穿插原声和解说片段
|
||||
- 第一个片段必须是原声,时长不少于20秒
|
||||
- 选择整个视频中最精彩的片段作为开场
|
||||
|
||||
4. 创作重点:
|
||||
- 确保解说与画面高度匹配
|
||||
- 巧妙安排原声和解说的交替,提升观众体验
|
||||
- 创造一个引人入胜、节奏紧凑的解说脚本
|
||||
|
||||
5. 注意事项:
|
||||
- 严格遵守JSON格式,不包含任何注释或额外标记
|
||||
- 充分利用你的专业经验,创作出高质量、吸引人的解说内容
|
||||
|
||||
请基于以上要求,将提供的视频转录脚本和解说文案整合成一个专业、吸引人的解说脚本。你的创作将直接影响观众的观看体验,请发挥你的专业素养,创作出最佳效果。
|
||||
""" % (huamian, wenan)
|
||||
# prompt = """
|
||||
# 你是一位拥有10年丰富经验的影视解说创作专家。你的任务是根据提供的视频转录脚本和解说文案,创作一个引人入胜的解说脚本。请按照以下要求完成任务:
|
||||
#
|
||||
# 1. 输入数据:
|
||||
# - 视频转录脚本:包含时间戳、画面描述和人物台词
|
||||
# - 解说文案:需要你进行匹配和编排的内容
|
||||
# - 视频转录脚本和文案(由 XML 标记<PICTURE></PICTURE>和 <COPYWRITER></COPYWRITER>分隔)如下所示:
|
||||
# 视频转录脚本
|
||||
# <PICTURE>
|
||||
# %s
|
||||
# </PICTURE>
|
||||
# 文案:
|
||||
# <COPYWRITER>
|
||||
# %s
|
||||
# </COPYWRITER>
|
||||
#
|
||||
# 2. 输出要求:
|
||||
# - 格式:严格的JSON格式,可直接被json.loads()解析
|
||||
# - 结构:list[script],其中script为字典类型
|
||||
# - script字段:
|
||||
# {
|
||||
# "picture": "画面描述",
|
||||
# "timestamp": "时间戳",
|
||||
# "narration": "解说文案",
|
||||
# "OST": true/false
|
||||
# }
|
||||
#
|
||||
# 3. 匹配规则:
|
||||
# a) 时间戳匹配:
|
||||
# - 根据文案内容选择最合适的画面时间段
|
||||
# - 避免时间重叠,确保画面不重复出现
|
||||
# - 适当合并或删减片段,不要完全照搬转录脚本
|
||||
# b) 画面描述:与转录脚本保持一致
|
||||
# c) 解说文案:
|
||||
# - 当OST为true时,narration为空字符串
|
||||
# - 当OST为false时,narration为解说文案,但是要确保文案字数不要超过 30字,若文案较长,则添加到下一个片段
|
||||
# d) OST(原声):
|
||||
# - 按1:1比例穿插原声和解说片段
|
||||
# - 第一个片段必须是原声,时长不少于20秒
|
||||
# - 选择整个视频中最精彩的片段作为开场
|
||||
#
|
||||
# 4. 创作重点:
|
||||
# - 确保解说与画面高度匹配
|
||||
# - 巧妙安排原声和解说的交替,提升观众体验
|
||||
# - 创造一个引人入胜、节奏紧凑的解说脚本
|
||||
#
|
||||
# 5. 注意事项:
|
||||
# - 严格遵守JSON格式,不包含任何注释或额外标记
|
||||
# - 充分利用你的专业经验,创作出高质量、吸引人的解说内容
|
||||
#
|
||||
# 请基于以上要求,将提供的视频转录脚本和解说文案整合成一个专业、吸引人的解说脚本。你的创作将直接影响观众的观看体验,请发挥你的专业素养,创作出最佳效果。
|
||||
# """ % (huamian, wenan)
|
||||
try:
|
||||
response = _generate_response(prompt, llm_provider)
|
||||
logger.success("匹配成功")
|
||||
|
||||
@ -267,7 +267,6 @@ def save_clip_video(timestamp: str, origin_video: str, save_dir: str = "") -> di
|
||||
if not os.path.exists(save_dir):
|
||||
os.makedirs(save_dir)
|
||||
|
||||
# url_hash = utils.md5(str(uuid.uuid4()))
|
||||
video_id = f"vid-{timestamp.replace(':', '_')}"
|
||||
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)
|
||||
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):
|
||||
try:
|
||||
@ -297,20 +296,21 @@ 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, ) -> dict:
|
||||
def clip_videos(task_id: str, timestamp_terms: List[str], origin_video: str, progress_callback=None):
|
||||
"""
|
||||
剪辑视频
|
||||
Args:
|
||||
task_id: 任务id
|
||||
timestamp_terms: 需要剪辑的时间戳列表,如:['00:00-00:20', '00:36-00:40', '07:07-07:22']
|
||||
origin_video: 原视频路径
|
||||
progress_callback: 进度回调函数
|
||||
|
||||
Returns:
|
||||
剪辑后的视频路径
|
||||
"""
|
||||
video_paths = {}
|
||||
for item in timestamp_terms:
|
||||
logger.info(f"需要裁剪 '{origin_video}' 为 {len(timestamp_terms)} 个视频")
|
||||
total_items = len(timestamp_terms)
|
||||
for index, item in enumerate(timestamp_terms):
|
||||
material_directory = config.app.get("material_directory", "").strip()
|
||||
if material_directory == "task":
|
||||
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 = ""
|
||||
|
||||
try:
|
||||
logger.info(f"clip video: {item}")
|
||||
saved_video_path = save_clip_video(timestamp=item, origin_video=origin_video, save_dir=material_directory)
|
||||
if saved_video_path:
|
||||
logger.info(f"video saved: {saved_video_path}")
|
||||
video_paths.update(saved_video_path)
|
||||
|
||||
# 更新进度
|
||||
if progress_callback:
|
||||
progress_callback(index + 1, total_items)
|
||||
except Exception as e:
|
||||
logger.error(f"视频裁剪失败: {utils.to_json(item)} => {str(e)}")
|
||||
return {}
|
||||
|
||||
@ -48,7 +48,10 @@ def create(audio_file, subtitle_file: str = ""):
|
||||
)
|
||||
try:
|
||||
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:
|
||||
logger.error(
|
||||
@ -72,6 +75,7 @@ def create(audio_file, subtitle_file: str = ""):
|
||||
word_timestamps=True,
|
||||
vad_filter=True,
|
||||
vad_parameters=dict(min_silence_duration_ms=500),
|
||||
initial_prompt="以下是普通话的句子"
|
||||
)
|
||||
|
||||
logger.info(
|
||||
|
||||
@ -2,6 +2,7 @@ import math
|
||||
import json
|
||||
import os.path
|
||||
import re
|
||||
import traceback
|
||||
from os import path
|
||||
from loguru import logger
|
||||
|
||||
@ -323,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):
|
||||
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. 加载视频脚本")
|
||||
video_script_path = path.join(params.video_clip_json_path)
|
||||
# video_script_path = video_clip_json_path
|
||||
# 判断json文件是否存在
|
||||
if path.exists(video_script_path):
|
||||
try:
|
||||
@ -361,6 +363,7 @@ def start_subclip(task_id: str, params: VideoClipParams, subclip_path_videos):
|
||||
logger.error(f"无法读取视频json脚本,请检查配置是否正确。{e}")
|
||||
raise ValueError("无法读取视频json脚本,请检查配置是否正确")
|
||||
else:
|
||||
logger.error(f"video_script_path: {video_script_path} \n\n", traceback.format_exc())
|
||||
raise ValueError("解说脚本不存在!请检查配置是否正确。")
|
||||
|
||||
logger.info("\n\n## 2. 生成音频列表")
|
||||
|
||||
@ -1,9 +1,12 @@
|
||||
import locale
|
||||
import os
|
||||
import traceback
|
||||
|
||||
import requests
|
||||
import threading
|
||||
from typing import Any
|
||||
from loguru import logger
|
||||
import streamlit as st
|
||||
import json
|
||||
from uuid import uuid4
|
||||
import urllib3
|
||||
@ -11,6 +14,7 @@ from datetime import datetime, timedelta
|
||||
|
||||
from app.models import const
|
||||
from app.utils import check_script
|
||||
from app.services import material
|
||||
|
||||
urllib3.disable_warnings()
|
||||
|
||||
@ -372,10 +376,52 @@ def add_new_timestamps(scenes):
|
||||
|
||||
|
||||
def clean_model_output(output):
|
||||
"""
|
||||
模型输出包含 ```json 标记时的处理
|
||||
"""
|
||||
if "```json" in output:
|
||||
print("##########")
|
||||
output = output.replace("```json", "").replace("```", "")
|
||||
return output.strip()
|
||||
# 移除可能的代码块标记
|
||||
output = output.strip('```json').strip('```')
|
||||
# 移除开头和结尾的空白字符
|
||||
output = output.strip()
|
||||
return output
|
||||
|
||||
|
||||
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
|
||||
|
||||
210
webui.py
210
webui.py
@ -61,13 +61,11 @@ config_file = os.path.join(root_dir, "webui", ".streamlit", "webui.toml")
|
||||
system_locale = utils.get_system_locale()
|
||||
|
||||
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:
|
||||
st.session_state['video_plot'] = ''
|
||||
if 'ui_language' not in st.session_state:
|
||||
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():
|
||||
@ -124,7 +122,7 @@ def init_log():
|
||||
_lvl = "DEBUG"
|
||||
|
||||
def format_record(record):
|
||||
# 获取日志记录中的文件全路径
|
||||
# 获取日志记录中的文件全<EFBFBD><EFBFBD><EFBFBD>径
|
||||
file_path = record["file"].path
|
||||
# 将绝对路径转换为相对于项目根目录的路径
|
||||
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)
|
||||
|
||||
# 脚本文件 下拉框
|
||||
# <EFBFBD><EFBFBD>本文件 下拉框
|
||||
script_path = [(tr("Auto Generate"), ""), ]
|
||||
for file in script_list:
|
||||
display_name = file['file'].replace(root_dir, "")
|
||||
@ -282,8 +280,9 @@ with left_panel:
|
||||
options=range(len(script_path)), # 使用索引作为内部选项值
|
||||
format_func=lambda x: script_path[x][0] # 显示给用户的是标签
|
||||
)
|
||||
params.video_clip_json = script_path[selected_script_index][1]
|
||||
video_json_file = params.video_clip_json
|
||||
params.video_clip_json_path = script_path[selected_script_index][1]
|
||||
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 = []
|
||||
@ -301,18 +300,20 @@ with left_panel:
|
||||
})
|
||||
# 按创建时间降序排序
|
||||
video_list.sort(key=lambda x: x["ctime"], reverse=True)
|
||||
video_path = [("None", ""), (tr("Upload Local Files"), "local")]
|
||||
for code in [file['file'] for file in video_list]:
|
||||
video_path.append((code, code))
|
||||
video_path = [(tr("None"), ""), (tr("Upload Local Files"), "local")]
|
||||
for file in video_list:
|
||||
display_name = file['file'].replace(root_dir, "")
|
||||
video_path.append((display_name, file['file']))
|
||||
|
||||
# 视频文件
|
||||
selected_video_index = st.selectbox(tr("Video File"),
|
||||
index=0,
|
||||
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]
|
||||
config.app["video_origin_path"] = params.video_origin_path
|
||||
st.session_state['video_origin_path'] = params.video_origin_path
|
||||
|
||||
# 从本地上传 mp4 文件
|
||||
if params.video_origin_path == "local":
|
||||
@ -347,40 +348,73 @@ with left_panel:
|
||||
)
|
||||
|
||||
# 生成视频脚本
|
||||
st.session_state['script_generation_status'] = "开始生成视频脚本"
|
||||
if st.button(tr("Video Script Generate"), key="auto_generate_script"):
|
||||
with st.spinner("正在生成脚本..."):
|
||||
# 这里可以用 st.empty() 来动态更新文本
|
||||
progress_text = st.empty()
|
||||
progress_text.text("正在处理...")
|
||||
if st.session_state['video_clip_json_path']:
|
||||
generate_button_name = tr("Video Script Load")
|
||||
else:
|
||||
generate_button_name = tr("Video Script Generate")
|
||||
if st.button(generate_button_name, key="auto_generate_script"):
|
||||
progress_bar = st.progress(0)
|
||||
status_text = st.empty()
|
||||
|
||||
if video_json_file == "" and params.video_origin_path != "":
|
||||
progress_text.text("开始压缩...")
|
||||
# 使用大模型生成视频脚本
|
||||
script = llm.generate_script(
|
||||
video_path=params.video_origin_path,
|
||||
video_plot=video_plot,
|
||||
video_name=video_name,
|
||||
language=params.video_language,
|
||||
progress_text=progress_text
|
||||
)
|
||||
if script is None:
|
||||
st.error("生成脚本失败,请检查日志")
|
||||
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)
|
||||
def update_progress(progress: float, message: str = ""):
|
||||
progress_bar.progress(progress)
|
||||
if message:
|
||||
status_text.text(f"{progress}% - {message}")
|
||||
else:
|
||||
with open(video_json_file, 'r', encoding='utf-8') as f:
|
||||
script = f.read()
|
||||
st.session_state['video_clip_json'] = script
|
||||
cleaned_string = script.strip("```json").strip("```")
|
||||
st.session_state['video_script_list'] = json.loads(cleaned_string)
|
||||
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(
|
||||
video_path=params.video_origin_path,
|
||||
video_plot=video_plot,
|
||||
video_name=video_name,
|
||||
language=params.video_language,
|
||||
progress_callback=update_progress
|
||||
)
|
||||
if script is None:
|
||||
st.error("生成脚本失败,请检查日志")
|
||||
st.stop()
|
||||
else:
|
||||
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 = utils.clean_model_output(script)
|
||||
st.session_state['video_clip_json'] = json.loads(script)
|
||||
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(
|
||||
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
|
||||
)
|
||||
|
||||
@ -398,73 +432,43 @@ with left_panel:
|
||||
timestamp = datetime.datetime.now().strftime("%Y-%m%d-%H%M%S")
|
||||
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:
|
||||
data = utils.add_new_timestamps(json.loads(input_json))
|
||||
data = utils.add_new_timestamps(json.loads(video_clip_json_details))
|
||||
except Exception as err:
|
||||
st.error(f"视频脚本格式错误,请检查脚本是否符合 JSON 格式;{err} \n\n{traceback.format_exc()}")
|
||||
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 文件
|
||||
with open(save_path, 'w', encoding='utf-8') as file:
|
||||
json.dump(data, file, ensure_ascii=False, indent=4)
|
||||
# 将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.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("请先生成视频脚本"))
|
||||
|
||||
# st.rerun()
|
||||
|
||||
# 裁剪视频
|
||||
with button_columns[1]:
|
||||
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:
|
||||
@ -703,14 +707,16 @@ with st.expander(tr("Video Check"), expanded=False):
|
||||
# 可编辑的输入框
|
||||
text_panels = st.columns(2)
|
||||
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]:
|
||||
text2 = st.text_area(tr("Picture description"), value=initial_picture, height=20)
|
||||
logger.debug(initial_narration)
|
||||
text3 = st.text_area(tr("Narration"), value=initial_narration, height=100)
|
||||
text2 = st.text_area(tr("Picture description"), value=initial_picture, height=20,
|
||||
key=f"picture_{index}")
|
||||
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[index]['timestamp'] = text1
|
||||
video_list[index]['picture'] = text2
|
||||
@ -719,12 +725,12 @@ with st.expander(tr("Video Check"), expanded=False):
|
||||
for video in video_list:
|
||||
if 'path' in video:
|
||||
del video['path']
|
||||
# 更新session_state以确保更改被保存
|
||||
# 更新session_state以确保更改被保存
|
||||
st.session_state['video_clip_json'] = utils.to_json(video_list)
|
||||
# 替换原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)
|
||||
caijian()
|
||||
utils.cut_video(params, progress_callback=None)
|
||||
st.rerun()
|
||||
|
||||
# 开始按钮
|
||||
@ -735,13 +741,15 @@ if start_button:
|
||||
if st.session_state.get('video_script_json_path') is not None:
|
||||
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:
|
||||
st.error(tr("请先裁剪视频"))
|
||||
scroll_to_bottom()
|
||||
st.stop()
|
||||
if not params.video_clip_json:
|
||||
if not params.video_clip_json_path:
|
||||
st.error(tr("脚本文件不能为空"))
|
||||
scroll_to_bottom()
|
||||
st.stop()
|
||||
|
||||
@ -10,7 +10,7 @@
|
||||
"Auto Detect": "自动检测",
|
||||
"Auto Generate": "自动生成",
|
||||
"Video Name": "视频名称",
|
||||
"Video Script": "视频脚本(:blue[①可不填,使用AI生成 ②合理使用标点断句,有助于生成字幕])",
|
||||
"Video Script": "视频脚本(:blue[①使用AI生成 ②从本机加载])",
|
||||
"Save Script": "保存脚本",
|
||||
"Crop Video": "裁剪视频",
|
||||
"Video File": "视频文件(:blue[1️⃣支持上传视频文件(限制2G) 2️⃣大文件建议直接导入 ./resource/videos 目录])",
|
||||
@ -91,6 +91,7 @@
|
||||
"timestamp": "时间戳",
|
||||
"Picture description": "图片描述",
|
||||
"Narration": "视频文案",
|
||||
"Rebuild": "重新生成"
|
||||
"Rebuild": "重新生成",
|
||||
"Video Script Load": "加载视频脚本"
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user