diff --git a/app/services/llm.py b/app/services/llm.py
index d3742df..adb3f6d 100644
--- a/app/services/llm.py
+++ b/app/services/llm.py
@@ -31,7 +31,7 @@ Method = """
文案的前三句,是整部电影的概括总结,2-3句介绍后,开始叙述故事剧情!
推荐新手(新号)做:(盘点型)
盘点全球最恐怖的10部电影
-盘点全球最科幻的10部电影
+盘���全球最科幻的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后的青春启蒙,曾入选《��代》周刊,获得年度佳片第一,可在国内却被尘封多年,至今为止都无法在各大视频网站看到完整资源,他就是《xxxxxx》
8.这是一部让所有人看得荷尔蒙飙升的爽片……
9.他被成为世界上最虐心绝望的电影,至今无人敢看第二遍,很难想象,他是根据真实事件改编而来……
10.这大概是有史以来最令人不寒而栗的电影,当年一经放映,就点燃了无数人的怒火,不少观众不等影片放完,就愤然离场,它比《xxx》更让人绝望,比比《xxx》更让人xxx,能坚持看完全片的人,更是万中无一,包括我。甚至观影结束后,有无数人抵制投诉这部电影,认为影片的导演玩弄了他们的情感!他是顶级神作《xxxx》……
11.这是X国有史以来最高赞的一部悬疑电影,然而却因为某些原因,国内90%的人,没能看过这部片子,他就是《xxx》……
12.有这样一部电影,这辈子,你绝对不想再看第二遍,并不是它剧情烂俗,而是它的结局你根本承受不起/想象不到……甚至有80%的观众在观影途中情绪崩溃中途离场,更让许多同行都不想解说这部电影,他就是大名鼎鼎的暗黑神作《xxx》…
-13.它被誉为史上最牛悬疑片无数人在看完它时候,一个月不敢照镜子,这样一部仅适合部分年龄段观看的影片,究竟有什么样的魅力,竟然获得某瓣8.2的高分,很多人说这部电影到处都是看点,他就是《xxx》….
+13.它被誉为史上最牛悬疑片无数人在看完它时候,一个月不敢照镜��,这样一部仅适合部分年龄段观看的影片,究竟有什么样的魅力,竟然获得某瓣8.2的高分,很多人说这部电影到处都是看点,他就是《xxx》….
14.这是一部在某瓣上被70万人打出9.3分的高分的电影……到底是一部什么样的电影,能够在某瓣上被70万人打出9.3分的高分……
15.这是一部细思极恐的科幻大片,整部电影颠覆你的三观,它的名字叫……
16.史上最震撼的灾难片,每一点都不舍得快进的电影,他叫……
@@ -66,7 +66,7 @@ Method = """
2.这是一部印度高分悬疑片,
3.这部电影原在日本因为……而被下架,
4.这是韩国最恐怖的犯罪片,
-5.这是最近国产片评分最高的悬疑片
+5.这是最近国产片评分最高的悬疑��
以上均按照影片国家来区分,然后简单介绍下主题。就可以开始直接叙述作品。也是一个很不错的方法!
### 方式四:如何自由发挥
@@ -97,7 +97,7 @@ Method = """
后面水平越来越高的时候,可以进行人生道理的讲评。
比如:这部电影告诉我们……
-类似于哲理性质的,作为一个总结!
+类似于哲理性质��作为一个总结!
也可以把最后的影视反转,原生放出来,留下悬念。
比如:也可以总结下这部短片如何的好,推荐/值得大家去观看之类的话语。
@@ -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 标记和 分隔)如下所示:
- 视频转录脚本
-
- %s
-
- 文案:
-
- %s
-
-
-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 标记和 分隔)如下所示:
+# 视频转录脚本
+#
+# %s
+#
+# 文案:
+#
+# %s
+#
+#
+# 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("匹配成功")
diff --git a/app/services/material.py b/app/services/material.py
index d63e6fc..bc4d118 100644
--- a/app/services/material.py
+++ b/app/services/material.py
@@ -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 {}
diff --git a/app/services/subtitle.py b/app/services/subtitle.py
index b9894b0..c792667 100644
--- a/app/services/subtitle.py
+++ b/app/services/subtitle.py
@@ -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(
diff --git a/app/services/task.py b/app/services/task.py
index 946b4cd..78941f8 100644
--- a/app/services/task.py
+++ b/app/services/task.py
@@ -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. 生成音频列表")
diff --git a/app/utils/utils.py b/app/utils/utils.py
index e4ba419..3a0600f 100644
--- a/app/utils/utils.py
+++ b/app/utils/utils.py
@@ -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
diff --git a/webui.py b/webui.py
index c4853d8..4410c2d 100644
--- a/webui.py
+++ b/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):
- # 获取日志记录中的文件全路径
+ # 获取日志记录中的文件全���径
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)
- # 脚本文件 下拉框
+ # ��本文件 下拉框
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()
diff --git a/webui/i18n/zh.json b/webui/i18n/zh.json
index dc1da54..aa588fd 100644
--- a/webui/i18n/zh.json
+++ b/webui/i18n/zh.json
@@ -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": "加载视频脚本"
}
}
\ No newline at end of file