(webfeatui): 重构并优化视频解说文案生成功能

-优化了基本设置组件,增加了对 DeepSeek 和 Siliconflow 的支持- 重构了 generate_narration_script 模块,提高了代码可读性和可维护性
-改进了 generate_script_docu 工具,简化了文案生成流程- 添加了异常处理和日志记录,提高了系统稳定性
This commit is contained in:
linyq 2025-05-08 17:02:02 +08:00
parent e1b694824b
commit dac23c7c31
3 changed files with 196 additions and 99 deletions

View File

@ -11,6 +11,7 @@
import json
import os
import traceback
from openai import OpenAI
def parse_frame_analysis_to_markdown(json_file_path):
@ -50,16 +51,11 @@ def parse_frame_analysis_to_markdown(json_file_path):
time_range = summary.get('time_range', '')
batch_summary = summary.get('summary', '')
# 处理可能过长的文本行,保证格式对齐
batch_summary_lines = [batch_summary[i:i+80] for i in range(0, len(batch_summary), 80)]
markdown += f"## 片段 {i}\n"
markdown += f"- 时间范围:{time_range}\n"
# 添加片段描述,处理长文本
markdown += f"- 片段描述:{batch_summary_lines[0]}\n" if batch_summary_lines else f"- 片段描述:\n"
for line in batch_summary_lines[1:]:
markdown += f" {line}\n"
# 添加片段描述
markdown += f"- 片段描述:{batch_summary}\n" if batch_summary else f"- 片段描述:\n"
markdown += "- 详细描述:\n"
@ -69,11 +65,8 @@ def parse_frame_analysis_to_markdown(json_file_path):
timestamp = frame.get('timestamp', '')
observation = frame.get('observation', '')
# 处理可能过长的观察文本并确保observation不为空
observation_lines = [observation[i:i+80] for i in range(0, len(observation), 80)] if observation else [""]
markdown += f" - {timestamp}: {observation_lines[0] if observation_lines else ''}\n"
for line in observation_lines[1:]:
markdown += f" {line}\n"
# 直接使用原始文本,不进行分割
markdown += f" - {timestamp}: {observation}\n" if observation else f" - {timestamp}: \n"
markdown += "\n"
@ -83,16 +76,167 @@ def parse_frame_analysis_to_markdown(json_file_path):
return f"处理JSON文件时出错: {traceback.format_exc()}"
if __name__ == '__main__':
video_frame_description_path = "/Users/apple/Desktop/home/NarratoAI/storage/temp/analysis/frame_analysis_20250508_1139.json"
def generate_narration(markdown_content, api_key, base_url, model):
"""
调用OpenAI API根据视频帧分析的Markdown内容生成解说文案
:param markdown_content: Markdown格式的视频帧分析内容
:param api_key: OpenAI API密钥
:param base_url: API基础URL如果使用非官方API
:param model: 使用的模型名称
:return: 生成的解说文案
"""
try:
# 构建提示词
prompt = """
我是一名荒野建造解说的博主以下是一些同行的对标文案请你深度学习并总结这些文案的风格特点跟内容特点
<example_text_1>
解压助眠的天花板就是荒野建造沉浸丝滑的搭建过程可以说每一帧都是极致享受我保证强迫症来了都找不出一丁点毛病更别说全屋严丝合缝的拼接工艺还能轻松抵御零下二十度气温让你居住的每一天都温暖如春
在家闲不住的西姆今天也打算来一次野外建造行走没多久他就发现许多倒塌的树任由它们自生自灭不如将其利用起来想到这他就开始挥舞铲子要把地基挖掘出来虽然每次只能挖一点点但架不住他体能惊人没多长时间一个 2x3 的深坑就赫然出现这深度住他一人绰绰有余
随后他去附近收集来原木这些都是搭建墙壁的最好材料而在投入使用前自然要把表皮刮掉防止森林中的白蚁蛀虫处理好一大堆后西姆还在两端打孔使用木钉固定在一起这可不是用来做墙壁的而是做庇护所的承重柱只要木头间的缝隙足够紧密那搭建出的木屋就能足够坚固
每向上搭建一层他都会在中间塞入苔藓防寒保证不会泄露一丝热量其他几面也是用相同方法很快西姆就做好了三面墙壁每一根木头都极其工整保证强迫症来了都要点个赞再走
在继续搭建墙壁前西姆决定将壁炉制作出来毕竟森林夜晚的气温会很低保暖措施可是重中之重完成后他找来一块大树皮用来充当庇护所的大门而上面刮掉的木屑还能作为壁炉的引火物可以说再完美不过
测试了排烟没问题后他才开始搭建最后一面墙壁这一面要预留门和窗所以在搭建到一半后还需要在原木中间开出卡口让自己劈砍时能轻松许多此时只需将另外一根如法炮制两端拼接在一起后就是一扇大小适中的窗户而随着随后一层苔藓铺好最后一根原木落位这个庇护所的雏形就算完成
大门的安装他没选择用合页而是在底端雕刻出榫头门框上则雕刻出榫眼只能说西姆的眼就是一把尺这完全就是严丝合缝此时他才开始搭建屋顶这里西姆用的方法不同他先把最外围的原木固定好随后将原木平铺在上面就能得到完美的斜面屋顶等他将四周的围栏也装好后工整的屋顶看起来十分舒服西姆躺上去都不想动
稍作休息后他利用剩余的苔藓对屋顶的缝隙处密封可这样西姆觉得不够保险于是他找来一些黏土再次对原本的缝隙二次加工保管这庇护所冬天也暖和最后只需要平铺上枯叶以及挖掘出的泥土整个屋顶就算完成
考虑到庇护所的美观性自然少不了覆盖上苔藓翠绿的颜色看起来十分舒服就连门口的庭院旁他都移植了许多小树做点缀让这木屋与周边环境融为一体西姆才刚完成好这件事一场大雨就骤然降临好在此时的他已经不用淋雨更别说这屋顶防水十分不错室内没一点雨水渗透进来
等待温度回升的过程西姆利用墙壁本身的凹槽把床框镶嵌在上面只需要铺上苔藓以及自带的床单枕头一张完美的单人床就做好辛苦劳作一整天西姆可不会亏待自己他将自带的牛肉腌制好后直接放到壁炉中烤只需要等待三十分钟就能享受这美味的一顿
在辛苦建造一星期后他终于可以在自己搭建的庇护所中享受最纯正的野外露营后面西姆回家补给了一堆物资再次回来时森林已经大雪纷飞让他原本翠绿的小屋更换上了冬季限定皮肤好在内部设施没受什么影响和他离开时一样整洁
就是房间中已经没多少柴火让西姆今天又得劈柴寒冷干燥的天气让木头劈起来十分轻松没多久他就收集到一大堆这些足够燃烧好几天虽然此时外面大雪纷飞但小屋中却开始逐渐温暖这次他除了带来一些食物外还有几瓶调味料以及一整套被褥让自己的居住舒适度提高一大截
而秋天他有收集干草的缘故只需要塞入枕套中密封起来就能作为靠垫用就这居住条件比一般人在家过的还要奢侈趁着壁炉木头变木炭的过程西姆则开始不紧不慢的处理食物他取出一块牛排改好花刀以后撒上一堆调料腌制起来接着用锡纸包裹好放到壁炉中直接炭烤搭配上自带的红酒是一个非常好的选择
随着时间来到第二天外面的积雪融化了不少西姆简单做顿煎蛋补充体力后决定制作一个室外篝火堆用来晚上驱散周边野兽搭建这玩意没什么技巧只需要找到一大堆木棍利用大树的夹缝将其掰弯然后将其堆积在一起就是一个简易版的篝火堆看这外形有点像帐篷好在西姆没想那么多
等待天色暗淡下来后他才来到室外将其点燃顺便处理下多余的废料只可惜这场景没朋友陪在身边对西姆来说可能是个遗憾而哪怕森林只有他一个人都依旧做了好几个小时等到里面的篝火彻底燃尽后西姆还找来雪球覆盖到上面将火熄灭这防火意识可谓十分好最后在室内二十五度的高温下裹着被子睡觉
</example_text_1>
<example_text_2>
解压助眠的天花板就是荒野建造沉浸丝滑的搭建过程每一帧都是极致享受全屋严丝合缝的拼接工艺能轻松抵御零下二十度气温居住体验温暖如春
在家闲不住的西姆开启野外建造他发现倒塌的树决定加以利用先挖掘出 2x3 的深坑作为地基接着收集原木刮掉表皮防白蚁蛀虫打孔用木钉固定制作承重柱搭建墙壁时每一层都塞入苔藓防寒很快做好三面墙
为应对森林夜晚低温西姆制作壁炉用大树皮当大门刮下的木屑做引火物搭建最后一面墙时预留门窗通过在原木中间开口拼接做出窗户大门采用榫卯结构安装严丝合缝
搭建屋顶时先固定外围原木再平铺原木形成斜面屋顶之后用苔藓黏土密封缝隙铺上枯叶和泥土为美观在木屋覆盖苔藓移植小树点缀完工时遇大雨木屋防水良好
西姆利用墙壁凹槽镶嵌床框铺上苔藓床单枕头做成床劳作一天后他用壁炉烤牛肉享用建造一星期后他开始野外露营
后来西姆回家补给物资回来时森林大雪纷飞他劈柴储备带回食物调味料和被褥提高居住舒适度还用干草做靠垫他用壁炉烤牛排搭配红酒
第二天积雪融化西姆制作室外篝火堆防野兽用大树夹缝掰弯木棍堆积而成晚上点燃处理废料结束后用雪球灭火最后在室内二十五度的环境中裹被入睡
</example_text_2>
<example_text_3>
如果战争到来这个深埋地下十几米的庇护所绝对是 bug 般的存在即使被敌人发现还能通过快速通道一秒逃出里面不仅有竹子地暖地下水井还自制抽水机在解决用水问题的同时甚至自研无土栽培技术过上完全自给自足的生活
阿伟的老婆美如花但阿伟从来不回家来到野外他乐哈哈一言不合就开挖众所周知当战争来临时地下堡垒的安全性是最高的阿伟苦苦研习两载半只为练就一身挖洞本领在这双逆天麒麟臂的加持下如此坚硬的泥土都只能当做炮灰
得到了充足的空间后他便开始对这些边缘进行打磨随后阿伟将细线捆在木棍上以此描绘出圆柱的轮廓接着再一点点铲掉多余的部分虽然是由泥土一体式打造但这样的桌子保准用上千年都不成问题
考虑到十几米的深度进出非常不方便于是阿伟找来两根长达 66.6 米的木头打算为庇护所打造一条快速通道只见他将木桩牢牢地插入地下并顺着洞口的方向延伸出去直到贯穿整个山洞接着在每个木桩的连接处钉入铁钉确保轨道不能有一毫米的偏差完成后再制作一个木质框架从而达到前后滑动的效果
不得不说阿伟这手艺简直就是大钢管子杵青蛙在上面放上一个木制的车斗还能加快搬运泥土的速度没多久庇护所的内部就已经初见雏形为了住起来更加舒适还需要为自己打造一张床虽然深处的泥土同样很坚固但好处就是不用担心垮塌的风险
阿伟不仅设计了更加符合人体工学的拱形并且还在一旁雕刻处壁龛就是这氛围怎么看着有点不太吉利别看阿伟一身腱子肉但这身体里的艺术细菌可不少每个边缘的地方他都做了精雕细琢瞬间让整个卧室的颜值提升一大截
住在地下的好处就是房子面积全靠挖每平方消耗两个半馒头不仅没有了房贷的压力就连买墓地的钱也省了阿伟将中间的墙壁挖空从而得到取暖的壁炉当然最重要的还有排烟问题要想从上往下打通十几米的山体是件极其困难的事好在阿伟年轻时报过忆坤年的古墓派补习班这打洞技术堪比隔壁学校的土拨鼠专业虽然深度长达十几米但排烟效果却一点不受影响一个字专业
随后阿伟继续对壁炉底部雕刻打通了底部放柴火的空间并制作出放锅的灶头完成后阿伟从侧面将壁炉打通并制作出一条导热的通道以此连接到床铺的位置毕竟住在这么一个风湿宝地不注意保暖除湿很容易得老寒腿
阿伟在床面上挖出一条条管道以便于温度能传输到床的每个角落接下来就可以根据这些通道的长度裁切出同样长短的竹子根据竹筒的大小凿出相互连接的孔洞最后再将竹筒内部打通以达到温度传送的效果
而后阿伟将这些管道安装到凹槽内在他严谨的制作工艺下每根竹子刚好都能镶嵌进去在铺设床面之前还需要用木塞把圆孔堵住防止泥土掉落进管道泥土虽然不能隔绝湿气但却是十分优良的导热材料等他把床面都压平后就可以小心的将这些木塞拔出来最后再用黏土把剩余的管道也遮盖起来直到整个墙面恢复原样
接下来还需要测试一下加热效果当他把火点起来后温度很快就传送到了管道内把火力一点点加大直到热气流淌到更远的床面随着小孔里的青烟冒出也预示着阿伟的地暖可以投入使用而后阿伟制作了一些竹条并用细绳将它们喜结连理
千里之行始于足下美好的家园要靠自己双手打造明明可以靠才艺吃饭的阿伟偏偏要用八块腹肌征服大家就问这样的男人哪个野生婆娘不喜欢完成后阿伟还用自己 35 码的大腚感受了一下真烫
随后阿伟来到野区找到一根上好的雷击木他当即就把木头咔嚓成两段并取下两节较为完整的带了回去刚好能和圆桌配套另外一个在里面凿出凹槽并插入木棍连接得到一个夯土的木锤住过农村的小伙伴都知道这样夯出来的地面堪比水泥地不仅坚硬耐磨还不用担心脚底打滑忙碌了一天的阿伟已经饥渴难耐拿出野生小烤肠安安心心住新房光脚爬上大热炕一觉能睡到天亮
第二天阿伟打算将房间扩宽毕竟吃住的地方有了还要解决个人卫生的问题阿伟在另一侧增加了一个房间他打算将这里打造成洗澡的地方为了防止泥土垮塌他将顶部做成圆弧形等挖出足够的空间后旁边的泥土已经堆成了小山
为了方便清理这些泥土阿伟在之前的轨道增加了转弯交接处依然是用铁钉固定一直延伸到房间的最里面有了运输车的帮助这些成吨的泥土也能轻松的运送出去并且还能体验过山车的感觉很快他就完成了清理工作
为了更方便的在里面洗澡他将底部一点点挖空这么大的浴缸看来阿伟并不打算一个人住完成后他将墙面雕刻的凹凸有致让这里看起来更加豪华接着用洛阳铲挖出排水口并用一根相同大小的竹筒作为开关
由于四周都是泥土还不能防水阿伟特意找了一些白蚁巢用来制作可以防水的野生水泥现在就可以将里里外外能接触到水的地方都涂抹一遍细心的阿伟还找来这种 500 克一斤的鹅卵石对池子表面进行装饰
没错水源问题阿伟早已经考虑在内他打算直接在旁边挖个水井毕竟已经挖了这么深再向下挖一挖应该就能到达地下水的深度经过几日的奋战能看得出阿伟已经消瘦了不少但一想到马上就能拥有的豪宅他直接化身为无情的挖土机器很快就挖到了好几米的深度
考虑到自己的弹跳力有限阿伟在一旁定入木桩然后通过绳子爬上爬下随着深度越来越深井底已经开始渗出水来这也预示着打井成功没多久这里面将渗满泉水仅凭一次就能挖到水源看来这里还真是块风湿宝地
随后阿伟在井口四周挖出凹槽以便于井盖的安置这一量才知道井的深度已经达到了足足的 5 阿伟把木板组合在一起再沿着标记切掉多余部分他甚至还给井盖做了把手可是如何从这么深的井里打水还是个问题但从阿伟坚定的眼神来看他应该想到了解决办法
只见他将树桩锯成两半然后用凿子把里面一点点掏空另外一半也是如法炮制接着还要在底部挖出圆孔要想成功将水从 5 米深的地方抽上来那就不得不提到大家熟知的勾股定理没错这跟勾股定理没什么关系
阿伟给竹筒做了一个木塞并在里面打上安装连接轴的孔为了增加密闭性阿伟不得不牺牲了自己的 AJ剪出与木塞相同的大小后再用木钉固定住随后他收集了一些树胶并放到火上加热融化接下来就可以涂在木塞上增加使用寿命
现在将竹筒组装完成就可以利用虹吸原理将水抽上来完成后就可以把井盖盖上去再用泥土在上面覆盖现在就不用担心失足掉下去了
接下来阿伟去采集了一些大漆将它涂抹在木桶接缝处就能将其二合为一完了再接入旁边浴缸的入水口每个连接的地方都要做好密封不然后面很容易漏水随后就可以安装上活塞并用一根木桩作为省力杠杆根据空气压强的原理将井水抽上来
经过半小时的来回拉扯硕大的浴缸终于被灌满阿伟也是忍不住洗了把脸接下来还需要解决排水的问题阿伟在地上挖出沟渠一直贯穿到屋外然后再用竹筒从出水口连接每个接口处都要抹上胶水就连门外的出水口他都做了隐藏
在野外最重要的就是庇护所水源还有食物既然已经完成了前二者那么阿伟还需要拥有可持续发展的食物来源他先是在地上挖了两排地洞然后在每根竹筒的表面都打上无数孔洞这就是他打算用来种植的载体在此之前还需要用大火对竹筒进行杀菌消毒
趁着这时候他去搬了一麻袋的木屑先用芭蕉叶覆盖在上面再铺上厚厚的黏土隔绝温度在火焰的温度下能让里面的木屑达到生长条件
等到第二天所有材料都晾凉后阿伟才将竹筒内部掏空并将木屑一点点地塞入竹筒一切准备就绪就可以将竹筒插入提前挖好的地洞最后再往竹筒里塞入种子依靠房间内的湿度和温度就能达到大棚种植的效果稍加时日这些种子就会慢慢发芽
虽然暂时还吃不上自己培养的食物但好在阿伟从表哥贺强那里学到不少钓鱼本领哪怕只有一根小小的竹竿也能让他钓上两斤半的大鲶鱼新鲜的食材那肯定是少不了高温消毒的过程趁着鱼没熟阿伟直接爬进浴缸冰凉的井水瞬间洗去了身上的疲惫这一刻的阿伟是无比的享受
不久后鱼也烤得差不多了阿伟的生活现在可以说是有滋有味住在十几米的地下不仅能安全感满满哪怕遇到危险还能通过轨道快速逃生
<example_text_3>
<video_frame_description>
%s
</video_frame_description>
我正在尝试做这个内容的解说纪录片视频我需要你以 <video_frame_description> </video_frame_description> 中的内容为解说目标根据我刚才提供给你的对标文案 <example_text> 特点以及你总结的特点帮我生成一段关于荒野建造的解说文案文案需要符合平台受欢迎的解说风格请使用 json 格式进行输出使用 <output> 中的输出格式
<output>
{
"items": [
{
"_id": 1, # 唯一递增id
"timestamp": "00:00:05,390-00:00:10,430",
"picture": "画面描述",
"narration": "解说文案",
}
}
</output>
<restriction>
1. 只输出 json 内容不要输出其他任何说明性的文字
2. 解说文案的语言使用 简体中文
3. 严禁虚构画面所有画面只能从 <video_frame_description> 中摘取
</restriction>
""" % (markdown_content)
# 使用OpenAI SDK初始化客户端
client = OpenAI(
api_key=api_key,
base_url=base_url
)
# 使用SDK发送请求
response = client.chat.completions.create(
model=model,
messages=[
{"role": "system", "content": "你是一名专业的视频解说文案撰写专家。"},
{"role": "user", "content": prompt}
],
temperature=0.7,
response_format={"type": "json_object"},
)
# 提取生成的文案
if response.choices and len(response.choices) > 0:
narration_script = response.choices[0].message.content
# 打印消耗的tokens
print(response.usage.total_tokens)
return narration_script
else:
return "生成解说文案失败: 未获取到有效响应"
except Exception as e:
return f"调用API生成解说文案时出错: {traceback.format_exc()}"
if __name__ == '__main__':
text_provider = 'openai'
text_api_key = "sk-lyxttmuwgcmocfmupotkaenxnfofednrqappdrypwtrboang"
text_model = "deepseek-ai/DeepSeek-R1"
text_base_url = "https://api.siliconflow.cn/v1"
video_frame_description_path = "/Users/apple/Desktop/home/NarratoAI/storage/temp/analysis/frame_analysis_20250508_1139.json"
# 测试新的JSON文件
test_file_path = "/Users/apple/Desktop/home/NarratoAI/storage/temp/analysis/frame_analysis_20250508_1458.json"
markdown_output = parse_frame_analysis_to_markdown(test_file_path)
print(markdown_output)
# print(markdown_output)
# 输出到文件以便检查格式
output_file = "/Users/apple/Desktop/home/NarratoAI/storage/temp/narration_script.md"
with open(output_file, 'w', encoding='utf-8') as f:
f.write(markdown_output)
print(f"\n已将Markdown输出保存到: {output_file}")
# print(f"\n已将Markdown输出保存到: {output_file}")
# 生成解说文案
narration = generate_narration(
markdown_output,
text_api_key,
base_url=text_base_url,
model=text_model
)
# 保存解说文案
print(narration)
print(type(narration))
narration_file = "/Users/apple/Desktop/home/NarratoAI/storage/temp/final_narration_script.json"
with open(narration_file, 'w', encoding='utf-8') as f:
f.write(narration)
print(f"\n已将解说文案保存到: {narration_file}")

View File

@ -1,7 +1,10 @@
import traceback
import streamlit as st
import os
from app.config import config
from app.utils import utils
from loguru import logger
def render_basic_settings(tr):
@ -266,7 +269,7 @@ def test_text_model_connection(api_key, base_url, model_name, provider, tr):
elif provider.lower() == 'moonshot':
base_url = "https://api.moonshot.cn/v1"
elif provider.lower() == 'deepseek':
base_url = "https://api.deepseek.com/v1"
base_url = "https://api.deepseek.com"
# 构建测试URL
test_url = f"{base_url.rstrip('/')}/chat/completions"
@ -288,7 +291,7 @@ def test_text_model_connection(api_key, base_url, model_name, provider, tr):
"messages": [
{"role": "user", "content": "直接回复我文本'当前网络可用'"}
],
"max_tokens": 10
"stream": False
}
# 发送测试请求
@ -296,7 +299,6 @@ def test_text_model_connection(api_key, base_url, model_name, provider, tr):
test_url,
headers=headers,
json=test_data,
timeout=10
)
if response.status_code == 200:
@ -313,7 +315,7 @@ def render_text_llm_settings(tr):
st.subheader(tr("Text Generation Model Settings"))
# 文案生成模型提供商选择
text_providers = ['DeepSeek', 'OpenAI', 'Qwen', 'Moonshot', 'Gemini']
text_providers = ['DeepSeek', 'OpenAI', 'Siliconflow', 'Qwen', 'Moonshot', 'Gemini']
saved_text_provider = config.app.get("text_llm_provider", "DeepSeek").lower()
saved_provider_index = 0
@ -331,9 +333,9 @@ def render_text_llm_settings(tr):
config.app["text_llm_provider"] = text_provider
# 获取已保存的文本模型配置
text_api_key = config.app.get(f"text_{text_provider}_api_key", "")
text_base_url = config.app.get(f"text_{text_provider}_base_url", "")
text_model_name = config.app.get(f"text_{text_provider}_model_name", "")
text_api_key = config.app.get(f"text_{text_provider}_api_key")
text_base_url = config.app.get(f"text_{text_provider}_base_url")
text_model_name = config.app.get(f"text_{text_provider}_model_name")
# 渲染文本模型配置输入框
st_text_api_key = st.text_input(tr("Text API Key"), value=text_api_key, type="password")
@ -342,6 +344,8 @@ def render_text_llm_settings(tr):
# 添加测试按钮
if st.button(tr("Test Connection"), key="test_text_connection"):
logger.debug(st_text_base_url)
logger.debug(st_text_model_name)
with st.spinner(tr("Testing connection...")):
success, message = test_text_model_connection(
api_key=st_text_api_key,
@ -364,11 +368,11 @@ def render_text_llm_settings(tr):
if st_text_model_name:
config.app[f"text_{text_provider}_model_name"] = st_text_model_name
# Cloudflare 特殊配置
if text_provider == 'cloudflare':
st_account_id = st.text_input(
tr("Account ID"),
value=config.app.get(f"text_{text_provider}_account_id", "")
)
if st_account_id:
config.app[f"text_{text_provider}_account_id"] = st_account_id
# # Cloudflare 特殊配置
# if text_provider == 'cloudflare':
# st_account_id = st.text_input(
# tr("Account ID"),
# value=config.app.get(f"text_{text_provider}_account_id", "")
# )
# if st_account_id:
# config.app[f"text_{text_provider}_account_id"] = st_account_id

View File

@ -342,83 +342,32 @@ def generate_script_docu(params):
"""
4. 生成文案
"""
update_progress(70, "正在生成脚本...")
logger.info("开始准备生成解说文案")
update_progress(80, "正在生成文案...")
from app.services.generate_narration_script import parse_frame_analysis_to_markdown, generate_narration
# 从配置中获取文本生成相关配置
text_provider = config.app.get('text_llm_provider', 'gemini').lower()
text_api_key = config.app.get(f'text_{text_provider}_api_key')
text_model = config.app.get(f'text_{text_provider}_model_name')
text_base_url = config.app.get(f'text_{text_provider}_base_url')
# 构建帧内容列表
frame_content_list = []
prev_batch_files = None
# 整理帧分析数据
markdown_output = parse_frame_analysis_to_markdown(analysis_json_path)
# 使用合并后的观察结果构建帧内容列表
if merged_frame_observations:
for obs in merged_frame_observations:
frame_content = {
"_id": obs.get("frame_number", 0), # 使用全局连续的帧编号作为ID
"timestamp": obs.get("timestamp", ""),
"picture": obs.get("observation", ""),
"narration": "",
"OST": 2,
"timestamp_seconds": obs.get("timestamp_seconds", 0)
}
frame_content_list.append(frame_content)
logger.debug(f"添加帧内容: ID={obs.get('frame_number', 0)}, 时间={obs.get('timestamp', '')}, 描述长度={len(obs.get('observation', ''))}")
else:
# 兼容旧的处理方式,如果没有合并后的观察结果
for i, result in enumerate(results):
if 'error' in result:
continue
batch_files = get_batch_files(keyframe_files, result, vision_batch_size)
_, _, timestamp_range = get_batch_timestamps(batch_files, prev_batch_files)
frame_content = {
"_id": i + 1,
"timestamp": timestamp_range,
"picture": result['response'],
"narration": "",
"OST": 2
}
frame_content_list.append(frame_content)
logger.debug(f"添加帧内容: 时间范围={timestamp_range}, 分析结果长度={len(result['response'])}")
# 更新上一个批次的文件
prev_batch_files = batch_files
if not frame_content_list:
raise Exception("没有有效的帧内容可以处理")
# ===================开始生成文案===================
update_progress(80, "正在生成文案...")
# 校验配置
api_params = {
"vision_api_key": vision_api_key,
"vision_model_name": vision_model,
"vision_base_url": vision_base_url or "",
"text_api_key": text_api_key,
"text_model_name": text_model,
"text_base_url": text_base_url or ""
}
chekc_video_config(api_params)
custom_prompt = st.session_state.get('custom_prompt', '')
processor = ScriptProcessor(
model_name=text_model,
api_key=text_api_key,
prompt=custom_prompt,
base_url=text_base_url or "",
video_theme=st.session_state.get('video_theme', '')
# 生成文案
# 生成解说文案
narration = generate_narration(
markdown_output,
text_api_key,
base_url=text_base_url,
model=text_model
)
# 处理帧内容生成脚本
script_result = processor.process_frames(frame_content_list)
narration_dict = json.loads(narration)['items']
# 为 narration_dict 中每个 item 新增一个 OST: 2 的字段, 代表保留原声和配音
narration_dict = [{**item, "OST": 2} for item in narration_dict]
logger.debug(f"解说文案创作完成:\n{"\n".join([item['narration'] for item in narration_dict])}")
# 结果转换为JSON字符串
script = json.dumps(script_result, ensure_ascii=False, indent=2)
script = json.dumps(narration_dict, ensure_ascii=False, indent=2)
except Exception as e:
logger.exception(f"大模型处理过程中发生错误\n{traceback.format_exc()}")