diff --git a/app/services/SDE/short_drama_explanation.py b/app/services/SDE/short_drama_explanation.py index c563171..439f63c 100644 --- a/app/services/SDE/short_drama_explanation.py +++ b/app/services/SDE/short_drama_explanation.py @@ -15,7 +15,8 @@ from typing import Dict, Any, Optional from loguru import logger from app.config import config from app.utils.utils import get_uuid, storage_dir -from app.services.SDE.prompt import subtitle_plot_analysis_v1, plot_writing +# 导入新的提示词管理系统 +from app.services.prompts import PromptManager class SubtitleAnalyzer: @@ -49,7 +50,15 @@ class SubtitleAnalyzer: self.provider = provider or self._detect_provider() # 设置提示词模板 - self.prompt_template = custom_prompt or subtitle_plot_analysis_v1 + if custom_prompt: + self.prompt_template = custom_prompt + else: + # 使用新的提示词管理系统 + self.prompt_template = PromptManager.get_prompt( + category="short_drama_narration", + name="plot_analysis", + parameters={} + ) # 根据提供商类型确定是否为原生Gemini self.is_native_gemini = self.provider.lower() == 'gemini' @@ -360,8 +369,15 @@ class SubtitleAnalyzer: Dict[str, Any]: 包含生成结果的字典 """ try: - # 构建完整提示词 - prompt = plot_writing % (short_name, plot_analysis) + # 使用新的提示词管理系统构建提示词 + prompt = PromptManager.get_prompt( + category="short_drama_narration", + name="script_generation", + parameters={ + "drama_name": short_name, + "plot_analysis": plot_analysis + } + ) if self.is_native_gemini: # 使用原生Gemini API格式 diff --git a/app/services/SDP/utils/step1_subtitle_analyzer_openai.py b/app/services/SDP/utils/step1_subtitle_analyzer_openai.py index 59ea3b0..b8f4d18 100644 --- a/app/services/SDP/utils/step1_subtitle_analyzer_openai.py +++ b/app/services/SDP/utils/step1_subtitle_analyzer_openai.py @@ -7,6 +7,8 @@ import os import json from .utils import load_srt +# 导入新的提示词管理系统 +from app.services.prompts import PromptManager def analyze_subtitle( @@ -45,28 +47,24 @@ def analyze_subtitle( base_url=base_url ) + # 使用新的提示词管理系统 + subtitle_analysis_prompt = PromptManager.get_prompt( + category="short_drama_editing", + name="subtitle_analysis", + parameters={ + "subtitle_content": subtitle_content, + "custom_clips": custom_clips + } + ) + messages = [ { "role": "system", - "content": """你是一名经验丰富的短剧编剧,擅长根据字幕内容按照先后顺序分析关键剧情,并找出 %s 个关键片段。 - 请返回一个JSON对象,包含以下字段: - { - "summary": "整体剧情梗概", - "plot_titles": [ - "关键剧情1", - "关键剧情2", - "关键剧情3", - "关键剧情4", - "关键剧情5", - "..." - ] - } - 请确保返回的是合法的JSON格式, 请确保返回的是 %s 个片段。 - """ % (custom_clips, custom_clips) + "content": "你是一名短剧编剧和内容分析师,擅长从字幕中提取剧情要点和关键情节。" }, { "role": "user", - "content": f"srt字幕如下:{subtitle_content}" + "content": subtitle_analysis_prompt } ] # DeepSeek R1 和 V3 不支持 response_format=json_object @@ -90,40 +88,31 @@ def analyze_subtitle( print(json.dumps(summary_data, indent=4, ensure_ascii=False)) - # 获取爆点时间段分析 - prompt = f"""剧情梗概: - {summary_data['summary']} - - 需要定位的爆点内容: - """ + # 构建爆点标题列表 + plot_titles_text = "" print(f"找到 {len(summary_data['plot_titles'])} 个片段") for i, point in enumerate(summary_data['plot_titles'], 1): - prompt += f"{i}. {point}\n" + plot_titles_text += f"{i}. {point}\n" + + # 使用新的提示词管理系统 + plot_extraction_prompt = PromptManager.get_prompt( + category="short_drama_editing", + name="plot_extraction", + parameters={ + "subtitle_content": subtitle_content, + "plot_summary": summary_data['summary'], + "plot_titles": plot_titles_text + } + ) messages = [ { "role": "system", - "content": """你是一名短剧编剧,非常擅长根据字幕中分析视频中关键剧情出现的具体时间段。 - 请仔细阅读剧情梗概和爆点内容,然后在字幕中找出每个爆点发生的具体时间段和爆点前后的详细剧情。 - - 请返回一个JSON对象,包含一个名为"plot_points"的数组,数组中包含多个对象,每个对象都要包含以下字段: - { - "plot_points": [ - { - "timestamp": "时间段,格式为xx:xx:xx,xxx-xx:xx:xx,xxx", - "title": "关键剧情的主题", - "picture": "关键剧情前后的详细剧情描述" - } - ] - } - 请确保返回的是合法的JSON格式。""" + "content": "你是一名短剧编剧,非常擅长根据字幕中分析视频中关键剧情出现的具体时间段。" }, { "role": "user", - "content": f"""字幕内容: -{subtitle_content} - -{prompt}""" + "content": plot_extraction_prompt } ] # DeepSeek R1 和 V3 不支持 response_format=json_object diff --git a/app/services/generate_narration_script.py b/app/services/generate_narration_script.py index 475b80d..80fcf1a 100644 --- a/app/services/generate_narration_script.py +++ b/app/services/generate_narration_script.py @@ -18,6 +18,8 @@ from loguru import logger # 导入新的LLM服务模块 - 确保提供商被注册 import app.services.llm # 这会触发提供商注册 from app.services.llm.migration_adapter import generate_narration as generate_narration_new +# 导入新的提示词管理系统 +from app.services.prompts import PromptManager def parse_frame_analysis_to_markdown(json_file_path): @@ -116,95 +118,20 @@ def _generate_narration_legacy(markdown_content, api_key, base_url, model): :return: 生成的解说文案 """ try: - # 构建提示词 - prompt = """ -我是一名荒野建造解说的博主,以下是一些同行的对标文案,请你深度学习并总结这些文案的风格特点跟内容特点: + # 使用新的提示词管理系统构建提示词 + prompt = PromptManager.get_prompt( + category="documentary", + name="narration_generation", + parameters={ + "video_frame_description": markdown_content + } + ) - -解压助眠的天花板就是荒野建造,沉浸丝滑的搭建过程可以说每一帧都是极致享受,我保证强迫症来了都找不出一丁点毛病。更别说全屋严丝合缝的拼接工艺,还能轻松抵御零下二十度气温,让你居住的每一天都温暖如春。 -在家闲不住的西姆今天也打算来一次野外建造,行走没多久他就发现许多倒塌的树,任由它们自生自灭不如将其利用起来。想到这他就开始挥舞铲子要把地基挖掘出来,虽然每次只能挖一点点,但架不住他体能惊人。没多长时间一个 2x3 的深坑就赫然出现,这深度住他一人绰绰有余。 -随后他去附近收集来原木,这些都是搭建墙壁的最好材料。而在投入使用前自然要把表皮刮掉,防止森林中的白蚁蛀虫。处理好一大堆后西姆还在两端打孔,使用木钉固定在一起。这可不是用来做墙壁的,而是做庇护所的承重柱。只要木头间的缝隙足够紧密,那搭建出的木屋就能足够坚固。 -每向上搭建一层,他都会在中间塞入苔藓防寒,保证不会泄露一丝热量。其他几面也是用相同方法,很快西姆就做好了三面墙壁,每一根木头都极其工整,保证强迫症来了都要点个赞再走。 -在继续搭建墙壁前西姆决定将壁炉制作出来,毕竟森林夜晚的气温会很低,保暖措施可是重中之重。完成后他找来一块大树皮用来充当庇护所的大门,而上面刮掉的木屑还能作为壁炉的引火物,可以说再完美不过。 -测试了排烟没问题后他才开始搭建最后一面墙壁,这一面要预留门和窗,所以在搭建到一半后还需要在原木中间开出卡口,让自己劈砍时能轻松许多。此时只需将另外一根如法炮制,两端拼接在一起后就是一扇大小适中的窗户。而随着随后一层苔藓铺好,最后一根原木落位,这个庇护所的雏形就算完成。 -大门的安装他没选择用合页,而是在底端雕刻出榫头,门框上则雕刻出榫眼,只能说西姆的眼就是一把尺,这完全就是严丝合缝。此时他才开始搭建屋顶。这里西姆用的方法不同,他先把最外围的原木固定好,随后将原木平铺在上面,就能得到完美的斜面屋顶。等他将四周的围栏也装好后,工整的屋顶看起来十分舒服,西姆躺上去都不想动。 -稍作休息后,他利用剩余的苔藓,对屋顶的缝隙处密封。可这样西姆觉得不够保险,于是他找来一些黏土,再次对原本的缝隙二次加工,保管这庇护所冬天也暖和。最后只需要平铺上枯叶,以及挖掘出的泥土,整个屋顶就算完成。 -考虑到庇护所的美观性,自然少不了覆盖上苔藓,翠绿的颜色看起来十分舒服。就连门口的庭院旁,他都移植了许多小树做点缀,让这木屋与周边环境融为一体。西姆才刚完成好这件事,一场大雨就骤然降临。好在此时的他已经不用淋雨,更别说这屋顶防水十分不错,室内没一点雨水渗透进来。 -等待温度回升的过程,西姆利用墙壁本身的凹槽,把床框镶嵌在上面,只需要铺上苔藓,以及自带的床单枕头,一张完美的单人床就做好。辛苦劳作一整天,西姆可不会亏待自己。他将自带的牛肉腌制好后,直接放到壁炉中烤,只需要等待三十分钟,就能享受这美味的一顿。 -在辛苦建造一星期后,他终于可以在自己搭建的庇护所中,享受最纯正的野外露营。后面西姆回家补给了一堆物资,再次回来时森林已经大雪纷飞,让他原本翠绿的小屋,更换上了冬季限定皮肤。好在内部设施没受什么影响,和他离开时一样整洁。 -就是房间中已经没多少柴火,让西姆今天又得劈柴。寒冷干燥的天气,让木头劈起来十分轻松。没多久他就收集到一大堆,这些足够燃烧好几天。虽然此时外面大雪纷飞,但小屋中却开始逐渐温暖。这次他除了带来一些食物外,还有几瓶调味料,以及一整套被褥,让自己的居住舒适度提高一大截。 -而秋天他有收集干草的缘故,只需要塞入枕套中密封起来,就能作为靠垫用。就这居住条件,比一般人在家过的还要奢侈。趁着壁炉木头变木炭的过程,西姆则开始不紧不慢的处理食物。他取出一块牛排,改好花刀以后,撒上一堆调料腌制起来。接着用锡纸包裹好,放到壁炉中直接炭烤,搭配上自带的红酒,是一个非常好的选择。 -随着时间来到第二天,外面的积雪融化了不少,西姆简单做顿煎蛋补充体力后,决定制作一个室外篝火堆,用来晚上驱散周边野兽。搭建这玩意没什么技巧,只需要找到一大堆木棍,利用大树的夹缝将其掰弯,然后将其堆积在一起,就是一个简易版的篝火堆。看这外形有点像帐篷,好在西姆没想那么多。 -等待天色暗淡下来后,他才来到室外将其点燃,顺便处理下多余的废料。只可惜这场景没朋友陪在身边,对西姆来说可能是个遗憾。而哪怕森林只有他一个人,都依旧做了好几个小时。等到里面的篝火彻底燃尽后,西姆还找来雪球,覆盖到上面将火熄灭,这防火意识可谓十分好。最后在室内二十五度的高温下,裹着被子睡觉。 - - -解压助眠的天花板就是荒野建造,沉浸丝滑的搭建过程每一帧都是极致享受,全屋严丝合缝的拼接工艺,能轻松抵御零下二十度气温,居住体验温暖如春。 -在家闲不住的西姆开启野外建造。他发现倒塌的树,决定加以利用。先挖掘出 2x3 的深坑作为地基,接着收集原木,刮掉表皮防白蚁蛀虫,打孔用木钉固定制作承重柱。搭建墙壁时,每一层都塞入苔藓防寒,很快做好三面墙。 -为应对森林夜晚低温,西姆制作壁炉,用大树皮当大门,刮下的木屑做引火物。搭建最后一面墙时预留门窗,通过在原木中间开口拼接做出窗户。大门采用榫卯结构安装,严丝合缝。 -搭建屋顶时,先固定外围原木,再平铺原木形成斜面屋顶,之后用苔藓、黏土密封缝隙,铺上枯叶和泥土。为美观,在木屋覆盖苔藓,移植小树点缀。完工时遇大雨,木屋防水良好。 -西姆利用墙壁凹槽镶嵌床框,铺上苔藓、床单枕头做成床。劳作一天后,他用壁炉烤牛肉享用。建造一星期后,他开始野外露营。 -后来西姆回家补给物资,回来时森林大雪纷飞。他劈柴储备,带回食物、调味料和被褥,提高居住舒适度,还用干草做靠垫。他用壁炉烤牛排,搭配红酒。 -第二天,积雪融化,西姆制作室外篝火堆防野兽。用大树夹缝掰弯木棍堆积而成,晚上点燃处理废料,结束后用雪球灭火,最后在室内二十五度的环境中裹被入睡。 - - -如果战争到来,这个深埋地下十几米的庇护所绝对是 bug 般的存在。即使被敌人发现,还能通过快速通道一秒逃出。里面不仅有竹子、地暖、地下水井,还自制抽水机。在解决用水问题的同时,甚至自研无土栽培技术,过上完全自给自足的生活。 -阿伟的老婆美如花,但阿伟从来不回家,来到野外他乐哈哈,一言不合就开挖。众所周知当战争来临时,地下堡垒的安全性是最高的。阿伟苦苦研习两载半,只为练就一身挖洞本领。在这双逆天麒麟臂的加持下,如此坚硬的泥土都只能当做炮灰。 -得到了充足的空间后,他便开始对这些边缘进行打磨。随后阿伟将细线捆在木棍上,以此描绘出圆柱的轮廓。接着再一点点铲掉多余的部分。虽然是由泥土一体式打造,但这样的桌子保准用上千年都不成问题。 -考虑到十几米的深度进出非常不方便,于是阿伟找来两根长达 66.6 米的木头,打算为庇护所打造一条快速通道。只见他将木桩牢牢地插入地下,并顺着洞口的方向延伸出去,直到贯穿整个山洞。接着在每个木桩的连接处钉入铁钉,确保轨道不能有一毫米的偏差。完成后再制作一个木质框架,从而达到前后滑动的效果。 -不得不说阿伟这手艺简直就是大钢管子杵青蛙。在上面放上一个木制的车斗,还能加快搬运泥土的速度。没多久庇护所的内部就已经初见雏形。为了住起来更加舒适,还需要为自己打造一张床。虽然深处的泥土同样很坚固,但好处就是不用担心垮塌的风险。 -阿伟不仅设计了更加符合人体工学的拱形,并且还在一旁雕刻处壁龛。就是这氛围怎么看着有点不太吉利。别看阿伟一身腱子肉,但这身体里的艺术细菌可不少。每个边缘的地方他都做了精雕细琢,瞬间让整个卧室的颜值提升一大截。 -住在地下的好处就是房子面积全靠挖,每平方消耗两个半馒头。不仅没有了房贷的压力,就连买墓地的钱也省了。阿伟将中间的墙壁挖空,从而得到取暖的壁炉。当然最重要的还有排烟问题,要想从上往下打通十几米的山体是件极其困难的事。好在阿伟年轻时报过忆坤年的古墓派补习班,这打洞技术堪比隔壁学校的土拨鼠专业。虽然深度长达十几米,但排烟效果却一点不受影响,一个字专业! -随后阿伟继续对壁炉底部雕刻,打通了底部放柴火的空间,并制作出放锅的灶头。完成后阿伟从侧面将壁炉打通,并制作出一条导热的通道,以此连接到床铺的位置。毕竟住在这么一个风湿宝地,不注意保暖除湿很容易得老寒腿。 -阿伟在床面上挖出一条条管道,以便于温度能传输到床的每个角落。接下来就可以根据这些通道的长度裁切出同样长短的竹子,根据竹筒的大小凿出相互连接的孔洞,最后再将竹筒内部打通,以达到温度传送的效果。 -而后阿伟将这些管道安装到凹槽内,在他严谨的制作工艺下,每根竹子刚好都能镶嵌进去。在铺设床面之前还需要用木塞把圆孔堵住,防止泥土掉落进管道。泥土虽然不能隔绝湿气,但却是十分优良的导热材料。等他把床面都压平后就可以小心的将这些木塞拔出来,最后再用黏土把剩余的管道也遮盖起来,直到整个墙面恢复原样。 -接下来还需要测试一下加热效果,当他把火点起来后,温度很快就传送到了管道内,把火力一点点加大,直到热气流淌到更远的床面。随着小孔里的青烟冒出,也预示着阿伟的地暖可以投入使用。而后阿伟制作了一些竹条,并用细绳将它们喜结连理。 -千里之行始于足下,美好的家园要靠自己双手打造。明明可以靠才艺吃饭的阿伟偏偏要用八块腹肌征服大家,就问这样的男人哪个野生婆娘不喜欢?完成后阿伟还用自己 35 码的大腚感受了一下,真烫! -随后阿伟来到野区找到一根上好的雷击木,他当即就把木头咔嚓成两段,并取下两节较为完整的带了回去,刚好能和圆桌配套。另外一个在里面凿出凹槽,并插入木棍连接,得到一个夯土的木锤。住过农村的小伙伴都知道,这样夯出来的地面堪比水泥地,不仅坚硬耐磨,还不用担心脚底打滑。忙碌了一天的阿伟已经饥渴难耐,拿出野生小烤肠,安安心心住新房,光脚爬上大热炕,一觉能睡到天亮。 -第二天阿伟打算将房间扩宽,毕竟吃住的地方有了,还要解决个人卫生的问题。阿伟在另一侧增加了一个房间,他打算将这里打造成洗澡的地方。为了防止泥土垮塌,他将顶部做成圆弧形,等挖出足够的空间后,旁边的泥土已经堆成了小山。 -为了方便清理这些泥土,阿伟在之前的轨道增加了转弯,交接处依然是用铁钉固定,一直延伸到房间的最里面。有了运输车的帮助,这些成吨的泥土也能轻松的运送出去,并且还能体验过山车的感觉。很快他就完成了清理工作。 -为了更方便的在里面洗澡,他将底部一点点挖空,这么大的浴缸,看来阿伟并不打算一个人住。完成后他将墙面雕刻的凹凸有致,让这里看起来更加豪华。接着用洛阳铲挖出排水口,并用一根相同大小的竹筒作为开关。 -由于四周都是泥土还不能防水,阿伟特意找了一些白蚁巢,用来制作可以防水的野生水泥。现在就可以将里里外外,能接触到水的地方都涂抹一遍。细心的阿伟还找来这种 500 克一斤的鹅卵石,对池子表面进行装饰。 -没错,水源问题阿伟早已经考虑在内,他打算直接在旁边挖个水井,毕竟已经挖了这么深,再向下挖一挖,应该就能到达地下水的深度。经过几日的奋战,能看得出阿伟已经消瘦了不少,但一想到马上就能拥有的豪宅,他直接化身为无情的挖土机器,很快就挖到了好几米的深度。 -考虑到自己的弹跳力有限,阿伟在一旁定入木桩,然后通过绳子爬上爬下。随着深度越来越深,井底已经开始渗出水来,这也预示着打井成功。没多久这里面将渗满泉水,仅凭一次就能挖到水源,看来这里还真是块风湿宝地。 -随后阿伟在井口四周挖出凹槽,以便于井盖的安置。这一量才知道,井的深度已经达到了足足的 5 米。阿伟把木板组合在一起,再沿着标记切掉多余部分,他甚至还给井盖做了把手。可是如何从这么深的井里打水还是个问题,但从阿伟坚定的眼神来看,他应该想到了解决办法。 -只见他将树桩锯成两半,然后用凿子把里面一点点掏空,另外一半也是如法炮制。接着还要在底部挖出圆孔,要想成功将水从 5 米深的地方抽上来,那就不得不提到大家熟知的勾股定理。没错,这跟勾股定理没什么关系。 -阿伟给竹筒做了一个木塞,并在里面打上安装连接轴的孔。为了增加密闭性,阿伟不得不牺牲了自己的 AJ,剪出与木塞相同的大小后,再用木钉固定住。随后他收集了一些树胶,并放到火上加热融化。接下来就可以涂在木塞上增加使用寿命。 -现在将竹筒组装完成,就可以利用虹吸原理将水抽上来。完成后就可以把井盖盖上去,再用泥土在上面覆盖,现在就不用担心失足掉下去了。 -接下来阿伟去采集了一些大漆,将它涂抹在木桶接缝处,就能将其二合为一。完了再接入旁边浴缸的入水口,每个连接的地方都要做好密封,不然后面很容易漏水。随后就可以安装上活塞,并用一根木桩作为省力杠杆,根据空气压强的原理将井水抽上来。 -经过半小时的来回拉扯,硕大的浴缸终于被灌满,阿伟也是忍不住洗了把脸。接下来还需要解决排水的问题,阿伟在地上挖出沟渠,一直贯穿到屋外,然后再用竹筒从出水口连接,每个接口处都要抹上胶水,就连门外的出水口他都做了隐藏。 -在野外最重要的就是庇护所、水源还有食物。既然已经完成了前二者,那么阿伟还需要拥有可持续发展的食物来源。他先是在地上挖了两排地洞,然后在每根竹筒的表面都打上无数孔洞,这就是他打算用来种植的载体。在此之前,还需要用大火对竹筒进行杀菌消毒。 -趁着这时候,他去搬了一麻袋的木屑,先用芭蕉叶覆盖在上面,再铺上厚厚的黏土隔绝温度。在火焰的温度下,能让里面的木屑达到生长条件。 -等到第二天所有材料都晾凉后,阿伟才将竹筒内部掏空,并将木屑一点点地塞入竹筒。一切准备就绪,就可以将竹筒插入提前挖好的地洞。最后再往竹筒里塞入种子,依靠房间内的湿度和温度,就能达到大棚种植的效果。稍加时日,这些种子就会慢慢发芽。 -虽然暂时还吃不上自己培养的食物,但好在阿伟从表哥贺强那里学到不少钓鱼本领,哪怕只有一根小小的竹竿,也能让他钓上两斤半的大鲶鱼。新鲜的食材,那肯定是少不了高温消毒的过程。趁着鱼没熟,阿伟直接爬进浴缸,冰凉的井水瞬间洗去了身上的疲惫。这一刻的阿伟是无比的享受。 -不久后鱼也烤得差不多了,阿伟的生活现在可以说是有滋有味。住在十几米的地下,不仅能安全感满满,哪怕遇到危险,还能通过轨道快速逃生。 - - -%s - -我正在尝试做这个内容的解说纪录片视频,我需要你以 中的内容为解说目标,根据我刚才提供给你的对标文案 特点,以及你总结的特点,帮我生成一段关于荒野建造的解说文案,文案需要符合平台受欢迎的解说风格,请使用 json 格式进行输出;使用 中的输出格式: - -{ - "items": [ - { - "_id": 1, # 唯一递增id - "timestamp": "00:00:05,390-00:00:10,430", - "picture": "画面描述", - "narration": "解说文案", - } -} - - - -1. 只输出 json 内容,不要输出其他任何说明性的文字 -2. 解说文案的语言使用 简体中文 -3. 严禁虚构画面,所有画面只能从 中摘取 - -""" % (markdown_content) # 使用OpenAI SDK初始化客户端 client = OpenAI( diff --git a/app/services/prompts/__init__.py b/app/services/prompts/__init__.py new file mode 100644 index 0000000..8477710 --- /dev/null +++ b/app/services/prompts/__init__.py @@ -0,0 +1,68 @@ +#!/usr/bin/env python +# -*- coding: UTF-8 -*- + +""" +@Project: NarratoAI +@File : __init__.py +@Author : AI Assistant +@Date : 2025/1/7 +@Description: 统一提示词管理模块 +""" + +from .manager import PromptManager +from .base import BasePrompt, VisionPrompt, TextPrompt, ParameterizedPrompt +from .registry import PromptRegistry +from .template import TemplateRenderer +from .validators import PromptOutputValidator +from .exceptions import ( + PromptError, + PromptNotFoundError, + PromptValidationError, + TemplateRenderError +) + +# 版本信息 +__version__ = "1.0.0" +__author__ = "AI Assistant" + +# 导出的公共接口 +__all__ = [ + # 核心管理器 + "PromptManager", + + # 基础类 + "BasePrompt", + "VisionPrompt", + "TextPrompt", + "ParameterizedPrompt", + + # 工具类 + "PromptRegistry", + "TemplateRenderer", + "PromptOutputValidator", + + # 异常类 + "PromptError", + "PromptNotFoundError", + "PromptValidationError", + "TemplateRenderError", + + # 版本信息 + "__version__", + "__author__" +] + +# 模块初始化 +def initialize_prompts(): + """初始化提示词模块,注册所有提示词""" + from . import documentary + from . import short_drama_editing + from . import short_drama_narration + + # 注册各模块的提示词 + documentary.register_prompts() + short_drama_editing.register_prompts() + short_drama_narration.register_prompts() + +# 自动初始化 +initialize_prompts() diff --git a/app/services/prompts/base.py b/app/services/prompts/base.py new file mode 100644 index 0000000..8f65f60 --- /dev/null +++ b/app/services/prompts/base.py @@ -0,0 +1,180 @@ +#!/usr/bin/env python +# -*- coding: UTF-8 -*- + +""" +@Project: NarratoAI +@File : base.py +@Author : AI Assistant +@Date : 2025/1/7 +@Description: 提示词基础类定义 +""" + +from abc import ABC, abstractmethod +from typing import Dict, Any, Optional, List +from enum import Enum +from dataclasses import dataclass, field +from datetime import datetime + + +class ModelType(Enum): + """模型类型枚举""" + TEXT = "text" # 文本模型 + VISION = "vision" # 视觉模型 + MULTIMODAL = "multimodal" # 多模态模型 + + +class OutputFormat(Enum): + """输出格式枚举""" + TEXT = "text" # 纯文本 + JSON = "json" # JSON格式 + MARKDOWN = "markdown" # Markdown格式 + STRUCTURED = "structured" # 结构化数据 + + +@dataclass +class PromptMetadata: + """提示词元数据""" + name: str # 提示词名称 + category: str # 分类 + version: str # 版本 + description: str # 描述 + model_type: ModelType # 适用的模型类型 + output_format: OutputFormat # 输出格式 + author: str = "AI Assistant" # 作者 + created_at: datetime = field(default_factory=datetime.now) # 创建时间 + updated_at: datetime = field(default_factory=datetime.now) # 更新时间 + tags: List[str] = field(default_factory=list) # 标签 + parameters: List[str] = field(default_factory=list) # 支持的参数列表 + + +class BasePrompt(ABC): + """提示词基础类""" + + def __init__(self, metadata: PromptMetadata): + self.metadata = metadata + self._template = None + self._system_prompt = None + self._examples = [] + + @property + def name(self) -> str: + """获取提示词名称""" + return self.metadata.name + + @property + def category(self) -> str: + """获取提示词分类""" + return self.metadata.category + + @property + def version(self) -> str: + """获取提示词版本""" + return self.metadata.version + + @property + def model_type(self) -> ModelType: + """获取适用的模型类型""" + return self.metadata.model_type + + @property + def output_format(self) -> OutputFormat: + """获取输出格式""" + return self.metadata.output_format + + @abstractmethod + def get_template(self) -> str: + """获取提示词模板""" + pass + + def get_system_prompt(self) -> Optional[str]: + """获取系统提示词""" + return self._system_prompt + + def get_examples(self) -> List[str]: + """获取示例""" + return self._examples.copy() + + def validate_parameters(self, parameters: Dict[str, Any]) -> bool: + """验证参数""" + required_params = set(self.metadata.parameters) + provided_params = set(parameters.keys()) + + missing_params = required_params - provided_params + if missing_params: + from .exceptions import TemplateRenderError + raise TemplateRenderError( + template_name=self.name, + error_message="缺少必需参数", + missing_params=list(missing_params) + ) + return True + + def render(self, parameters: Dict[str, Any] = None) -> str: + """渲染提示词""" + parameters = parameters or {} + + # 验证参数 + if self.metadata.parameters: + self.validate_parameters(parameters) + + # 渲染模板 + template = self.get_template() + try: + return template.format(**parameters) + except KeyError as e: + from .exceptions import TemplateRenderError + raise TemplateRenderError( + template_name=self.name, + error_message=f"模板参数错误: {str(e)}", + missing_params=[str(e)] + ) + + def to_dict(self) -> Dict[str, Any]: + """转换为字典""" + return { + "metadata": { + "name": self.metadata.name, + "category": self.metadata.category, + "version": self.metadata.version, + "description": self.metadata.description, + "model_type": self.metadata.model_type.value, + "output_format": self.metadata.output_format.value, + "author": self.metadata.author, + "created_at": self.metadata.created_at.isoformat(), + "updated_at": self.metadata.updated_at.isoformat(), + "tags": self.metadata.tags, + "parameters": self.metadata.parameters + }, + "template": self.get_template(), + "system_prompt": self.get_system_prompt(), + "examples": self.get_examples() + } + + +class TextPrompt(BasePrompt): + """文本模型专用提示词""" + + def __init__(self, metadata: PromptMetadata): + if metadata.model_type not in [ModelType.TEXT, ModelType.MULTIMODAL]: + raise ValueError(f"TextPrompt只支持TEXT或MULTIMODAL模型类型,当前: {metadata.model_type}") + super().__init__(metadata) + + +class VisionPrompt(BasePrompt): + """视觉模型专用提示词""" + + def __init__(self, metadata: PromptMetadata): + if metadata.model_type not in [ModelType.VISION, ModelType.MULTIMODAL]: + raise ValueError(f"VisionPrompt只支持VISION或MULTIMODAL模型类型,当前: {metadata.model_type}") + super().__init__(metadata) + + +class ParameterizedPrompt(BasePrompt): + """支持参数化的提示词""" + + def __init__(self, metadata: PromptMetadata, required_parameters: List[str] = None): + super().__init__(metadata) + if required_parameters: + self.metadata.parameters.extend(required_parameters) + # 去重 + self.metadata.parameters = list(set(self.metadata.parameters)) diff --git a/app/services/prompts/documentary/__init__.py b/app/services/prompts/documentary/__init__.py new file mode 100644 index 0000000..3dcb63a --- /dev/null +++ b/app/services/prompts/documentary/__init__.py @@ -0,0 +1,33 @@ +#!/usr/bin/env python +# -*- coding: UTF-8 -*- + +""" +@Project: NarratoAI +@File : __init__.py +@Author : AI Assistant +@Date : 2025/1/7 +@Description: 纪录片解说提示词模块 +""" + +from .frame_analysis import FrameAnalysisPrompt +from .narration_generation import NarrationGenerationPrompt +from ..manager import PromptManager + + +def register_prompts(): + """注册纪录片解说相关的提示词""" + + # 注册视频帧分析提示词 + frame_analysis_prompt = FrameAnalysisPrompt() + PromptManager.register_prompt(frame_analysis_prompt, is_default=True) + + # 注册解说文案生成提示词 + narration_prompt = NarrationGenerationPrompt() + PromptManager.register_prompt(narration_prompt, is_default=True) + + +__all__ = [ + "FrameAnalysisPrompt", + "NarrationGenerationPrompt", + "register_prompts" +] diff --git a/app/services/prompts/documentary/frame_analysis.py b/app/services/prompts/documentary/frame_analysis.py new file mode 100644 index 0000000..ec1c478 --- /dev/null +++ b/app/services/prompts/documentary/frame_analysis.py @@ -0,0 +1,67 @@ +#!/usr/bin/env python +# -*- coding: UTF-8 -*- + +""" +@Project: NarratoAI +@File : frame_analysis.py +@Author : AI Assistant +@Date : 2025/1/7 +@Description: 纪录片视频帧分析提示词 +""" + +from ..base import VisionPrompt, PromptMetadata, ModelType, OutputFormat + + +class FrameAnalysisPrompt(VisionPrompt): + """纪录片视频帧分析提示词""" + + def __init__(self): + metadata = PromptMetadata( + name="frame_analysis", + category="documentary", + version="v1.0", + description="分析纪录片视频关键帧,提取画面内容和场景描述", + model_type=ModelType.VISION, + output_format=OutputFormat.JSON, + tags=["纪录片", "视频分析", "关键帧", "画面描述"], + parameters=["video_theme", "custom_instructions"] + ) + super().__init__(metadata) + + self._system_prompt = "你是一名专业的视频内容分析师,擅长分析纪录片视频帧内容,提取关键信息和场景描述。" + + def get_template(self) -> str: + return """请仔细分析这些视频关键帧图片,我需要你提供详细的画面分析。 + +视频主题:${video_theme} + +分析要求: +1. 按时间顺序分析每一帧画面 +2. 详细描述画面中的主要内容、人物、物体、环境 +3. 注意画面的构图、色彩、光线等视觉元素 +4. 识别画面中的关键动作或变化 +5. 提供准确的时间戳信息 + +${custom_instructions} + +请按照以下JSON格式输出分析结果: + +{{ + "analysis": [ + {{ + "timestamp": "00:00:05,390", + "picture": "详细的画面描述,包括场景、人物、物体、动作等", + "scene_type": "场景类型(如:建造、准备、完成等)", + "key_elements": ["关键元素1", "关键元素2"], + "visual_quality": "画面质量描述(构图、光线、色彩等)" + }} + ], + "summary": "整体视频内容概述", + "total_frames": "分析的帧数" +}} + +重要要求: +1. 只输出JSON格式,不要添加任何其他文字或代码块标记 +2. 画面描述要详细准确,为后续解说文案生成提供充分信息 +3. 时间戳必须准确对应视频帧 +4. 严禁虚构不存在的内容""" diff --git a/app/services/prompts/documentary/narration_generation.py b/app/services/prompts/documentary/narration_generation.py new file mode 100644 index 0000000..81eb24a --- /dev/null +++ b/app/services/prompts/documentary/narration_generation.py @@ -0,0 +1,82 @@ +#!/usr/bin/env python +# -*- coding: UTF-8 -*- + +""" +@Project: NarratoAI +@File : narration_generation.py +@Author : AI Assistant +@Date : 2025/1/7 +@Description: 纪录片解说文案生成提示词 +""" + +from ..base import TextPrompt, PromptMetadata, ModelType, OutputFormat + + +class NarrationGenerationPrompt(TextPrompt): + """纪录片解说文案生成提示词""" + + def __init__(self): + metadata = PromptMetadata( + name="narration_generation", + category="documentary", + version="v1.0", + description="根据视频帧分析结果生成纪录片解说文案,特别适用于荒野建造类内容", + model_type=ModelType.TEXT, + output_format=OutputFormat.JSON, + tags=["纪录片", "解说文案", "荒野建造", "文案生成"], + parameters=["video_frame_description"] + ) + super().__init__(metadata) + + self._system_prompt = "你是一名专业的短视频解说文案撰写专家,擅长创作引人入胜的纪录片解说内容。" + + def get_template(self) -> str: + return """我是一名荒野建造解说的博主,以下是一些同行的对标文案,请你深度学习并总结这些文案的风格特点跟内容特点: + + +解压助眠的天花板就是荒野建造,沉浸丝滑的搭建过程可以说每一帧都是极致享受,我保证强迫症来了都找不出一丁点毛病。更别说全屋严丝合缝的拼接工艺,还能轻松抵御零下二十度气温,让你居住的每一天都温暖如春。 +在家闲不住的西姆今天也打算来一次野外建造,行走没多久他就发现许多倒塌的树,任由它们自生自灭不如将其利用起来。想到这他就开始挥舞铲子要把地基挖掘出来,虽然每次只能挖一点点,但架不住他体能惊人。没多长时间一个 2x3 的深坑就赫然出现,这深度住他一人绰绰有余。 +随后他去附近收集来原木,这些都是搭建墙壁的最好材料。而在投入使用前自然要把表皮刮掉,防止森林中的白蚁蛀虫。处理好一大堆后西姆还在两端打孔,使用木钉固定在一起。这可不是用来做墙壁的,而是做庇护所的承重柱。只要木头间的缝隙足够紧密,那搭建出的木屋就能足够坚固。 +每向上搭建一层,他都会在中间塞入苔藓防寒,保证不会泄露一丝热量。其他几面也是用相同方法,很快西姆就做好了三面墙壁,每一根木头都极其工整,保证强迫症来了都要点个赞再走。 +在继续搭建墙壁前西姆决定将壁炉制作出来,毕竟森林夜晚的气温会很低,保暖措施可是重中之重。完成后他找来一块大树皮用来充当庇护所的大门,而上面刮掉的木屑还能作为壁炉的引火物,可以说再完美不过。 +测试了排烟没问题后他才开始搭建最后一面墙壁,这一面要预留门和窗,所以在搭建到一半后还需要在原木中间开出卡口,让自己劈砍时能轻松许多。此时只需将另外一根如法炮制,两端拼接在一起后就是一扇大小适中的窗户。而随着随后一层苔藓铺好,最后一根原木落位,这个庇护所的雏形就算完成。 + + + +解压助眠的天花板就是荒野建造,沉浸丝滑的搭建过程每一帧都是极致享受,全屋严丝合缝的拼接工艺,能轻松抵御零下二十度气温,居住体验温暖如春。 +在家闲不住的西姆开启野外建造。他发现倒塌的树,决定加以利用。先挖掘出 2x3 的深坑作为地基,接着收集原木,刮掉表皮防白蚁蛀虫,打孔用木钉固定制作承重柱。搭建墙壁时,每一层都塞入苔藓防寒,很快做好三面墙。 +为应对森林夜晚低温,西姆制作壁炉,用大树皮当大门,刮下的木屑做引火物。搭建最后一面墙时预留门窗,通过在原木中间开口拼接做出窗户。大门采用榫卯结构安装,严丝合缝。 +搭建屋顶时,先固定外围原木,再平铺原木形成斜面屋顶,之后用苔藓、黏土密封缝隙,铺上枯叶和泥土。为美观,在木屋覆盖苔藓,移植小树点缀。完工时遇大雨,木屋防水良好。 +西姆利用墙壁凹槽镶嵌床框,铺上苔藓、床单枕头做成床。劳作一天后,他用壁炉烤牛肉享用。建造一星期后,他开始野外露营。 +后来西姆回家补给物资,回来时森林大雪纷飞。他劈柴储备,带回食物、调味料和被褥,提高居住舒适度,还用干草做靠垫。他用壁炉烤牛排,搭配红酒。 +第二天,积雪融化,西姆制作室外篝火堆防野兽。用大树夹缝掰弯木棍堆积而成,晚上点燃处理废料,结束后用雪球灭火,最后在室内二十五度的环境中裹被入睡。 + + + +${video_frame_description} + + +我正在尝试做这个内容的解说纪录片视频,我需要你以 中的内容为解说目标,根据我刚才提供给你的对标文案特点,以及你总结的特点,帮我生成一段关于荒野建造的解说文案,文案需要符合平台受欢迎的解说风格,请使用 json 格式进行输出;使用 中的输出格式: + + +{{ + "items": [ + {{ + "_id": 1, + "timestamp": "00:00:05,390-00:00:10,430", + "picture": "画面描述", + "narration": "解说文案", + }} + ] +}} + + + +1. 只输出 json 内容,不要输出其他任何说明性的文字 +2. 解说文案的语言使用 简体中文 +3. 严禁虚构画面,所有画面只能从 中摘取 +4. 严禁虚构时间戳,所有时间戳只能从 中摘取 +5. 解说文案要生动有趣,符合荒野建造解说的风格特点 +6. 每个片段的解说文案要与画面内容高度匹配 +7. 保持解说的连贯性和故事性 +""" diff --git a/app/services/prompts/exceptions.py b/app/services/prompts/exceptions.py new file mode 100644 index 0000000..217c272 --- /dev/null +++ b/app/services/prompts/exceptions.py @@ -0,0 +1,79 @@ +#!/usr/bin/env python +# -*- coding: UTF-8 -*- + +""" +@Project: NarratoAI +@File : exceptions.py +@Author : AI Assistant +@Date : 2025/1/7 +@Description: 提示词管理模块异常定义 +""" + + +class PromptError(Exception): + """提示词模块基础异常类""" + pass + + +class PromptNotFoundError(PromptError): + """提示词未找到异常""" + + def __init__(self, category: str, name: str, version: str = None): + self.category = category + self.name = name + self.version = version + + if version: + message = f"提示词未找到: {category}.{name} (版本: {version})" + else: + message = f"提示词未找到: {category}.{name}" + + super().__init__(message) + + +class PromptValidationError(PromptError): + """提示词验证异常""" + + def __init__(self, message: str, validation_errors: list = None): + self.validation_errors = validation_errors or [] + super().__init__(message) + + +class TemplateRenderError(PromptError): + """模板渲染异常""" + + def __init__(self, template_name: str, error_message: str, missing_params: list = None): + self.template_name = template_name + self.error_message = error_message + self.missing_params = missing_params or [] + + message = f"模板渲染失败 '{template_name}': {error_message}" + if missing_params: + message += f" (缺少参数: {', '.join(missing_params)})" + + super().__init__(message) + + +class PromptRegistrationError(PromptError): + """提示词注册异常""" + + def __init__(self, category: str, name: str, reason: str): + self.category = category + self.name = name + self.reason = reason + + message = f"提示词注册失败 {category}.{name}: {reason}" + super().__init__(message) + + +class PromptVersionError(PromptError): + """提示词版本异常""" + + def __init__(self, category: str, name: str, version: str, reason: str): + self.category = category + self.name = name + self.version = version + self.reason = reason + + message = f"提示词版本错误 {category}.{name} v{version}: {reason}" + super().__init__(message) diff --git a/app/services/prompts/manager.py b/app/services/prompts/manager.py new file mode 100644 index 0000000..b396ac2 --- /dev/null +++ b/app/services/prompts/manager.py @@ -0,0 +1,287 @@ +#!/usr/bin/env python +# -*- coding: UTF-8 -*- + +""" +@Project: NarratoAI +@File : manager.py +@Author : AI Assistant +@Date : 2025/1/7 +@Description: 提示词管理器 +""" + +from typing import Dict, Any, List, Optional, Union +from loguru import logger + +from .base import BasePrompt, ModelType, OutputFormat +from .registry import get_registry +from .template import get_renderer +from .validators import PromptOutputValidator +from .exceptions import ( + PromptNotFoundError, + PromptValidationError, + TemplateRenderError +) + + +class PromptManager: + """提示词管理器 - 统一的提示词管理接口""" + + def __init__(self): + self._registry = get_registry() + self._renderer = get_renderer() + + @classmethod + def get_prompt(cls, + category: str, + name: str, + version: Optional[str] = None, + parameters: Optional[Dict[str, Any]] = None) -> str: + """ + 获取渲染后的提示词 + + Args: + category: 分类 + name: 名称 + version: 版本(可选,默认使用最新版本) + parameters: 模板参数(可选) + + Returns: + 渲染后的提示词字符串 + """ + instance = cls() + prompt_obj = instance._registry.get(category, name, version) + + try: + rendered = prompt_obj.render(parameters) + logger.debug(f"提示词渲染成功: {category}.{name} v{prompt_obj.version}") + return rendered + except Exception as e: + logger.error(f"提示词渲染失败: {category}.{name} - {str(e)}") + raise + + @classmethod + def get_prompt_object(cls, + category: str, + name: str, + version: Optional[str] = None) -> BasePrompt: + """ + 获取提示词对象 + + Args: + category: 分类 + name: 名称 + version: 版本(可选) + + Returns: + 提示词对象 + """ + instance = cls() + return instance._registry.get(category, name, version) + + @classmethod + def register_prompt(cls, prompt: BasePrompt, is_default: bool = True) -> None: + """ + 注册提示词 + + Args: + prompt: 提示词对象 + is_default: 是否设为默认版本 + """ + instance = cls() + instance._registry.register(prompt, is_default) + + @classmethod + def list_categories(cls) -> List[str]: + """列出所有分类""" + instance = cls() + return instance._registry.list_categories() + + @classmethod + def list_prompts(cls, category: str) -> List[str]: + """列出指定分类下的所有提示词""" + instance = cls() + return instance._registry.list_prompts(category) + + @classmethod + def list_versions(cls, category: str, name: str) -> List[str]: + """列出指定提示词的所有版本""" + instance = cls() + return instance._registry.list_versions(category, name) + + @classmethod + def exists(cls, category: str, name: str, version: Optional[str] = None) -> bool: + """检查提示词是否存在""" + instance = cls() + return instance._registry.exists(category, name, version) + + @classmethod + def search_prompts(cls, + keyword: str = None, + category: str = None, + model_type: ModelType = None, + output_format: OutputFormat = None) -> List[Dict[str, str]]: + """ + 搜索提示词 + + Args: + keyword: 关键词 + category: 分类过滤 + model_type: 模型类型过滤 + output_format: 输出格式过滤 + + Returns: + 匹配的提示词列表 + """ + instance = cls() + results = instance._registry.search(keyword, category, model_type, output_format) + + return [ + { + "category": cat, + "name": name, + "version": ver, + "full_name": f"{cat}.{name}", + "identifier": f"{cat}.{name}@{ver}" + } + for cat, name, ver in results + ] + + @classmethod + def get_stats(cls) -> Dict[str, Any]: + """获取统计信息""" + instance = cls() + registry_stats = instance._registry.get_stats() + + return { + "registry": registry_stats, + "categories": cls.list_categories(), + "total_categories": registry_stats["categories"], + "total_prompts": registry_stats["prompts"], + "total_versions": registry_stats["versions"] + } + + @classmethod + def validate_output(cls, + output: Union[str, Dict], + category: str, + name: str, + version: Optional[str] = None) -> Any: + """ + 验证提示词输出 + + Args: + output: 输出内容 + category: 提示词分类 + name: 提示词名称 + version: 提示词版本 + + Returns: + 验证后的数据 + """ + instance = cls() + prompt_obj = instance._registry.get(category, name, version) + + # 根据输出格式进行验证 + output_format = prompt_obj.metadata.output_format + + try: + if output_format == OutputFormat.JSON: + # 特殊处理解说文案和剧情分析 + if "narration" in name.lower() or "script" in name.lower(): + return PromptOutputValidator.validate_narration_script(output) + elif "plot" in name.lower() or "analysis" in name.lower(): + return PromptOutputValidator.validate_plot_analysis(output) + else: + return PromptOutputValidator.validate_json(output) + else: + return PromptOutputValidator.validate_by_format(output, output_format) + + except Exception as e: + logger.error(f"输出验证失败 {category}.{name}: {str(e)}") + raise PromptValidationError(f"输出验证失败: {str(e)}") + + @classmethod + def get_prompt_info(cls, category: str, name: str, version: Optional[str] = None) -> Dict[str, Any]: + """ + 获取提示词详细信息 + + Args: + category: 分类 + name: 名称 + version: 版本 + + Returns: + 提示词详细信息 + """ + instance = cls() + prompt_obj = instance._registry.get(category, name, version) + + return { + "metadata": { + "name": prompt_obj.metadata.name, + "category": prompt_obj.metadata.category, + "version": prompt_obj.metadata.version, + "description": prompt_obj.metadata.description, + "model_type": prompt_obj.metadata.model_type.value, + "output_format": prompt_obj.metadata.output_format.value, + "author": prompt_obj.metadata.author, + "created_at": prompt_obj.metadata.created_at.isoformat(), + "updated_at": prompt_obj.metadata.updated_at.isoformat(), + "tags": prompt_obj.metadata.tags, + "parameters": prompt_obj.metadata.parameters + }, + "template_preview": prompt_obj.get_template()[:500] + "..." if len(prompt_obj.get_template()) > 500 else prompt_obj.get_template(), + "system_prompt": prompt_obj.get_system_prompt(), + "examples_count": len(prompt_obj.get_examples()), + "has_parameters": bool(prompt_obj.metadata.parameters) + } + + @classmethod + def export_prompts(cls, category: Optional[str] = None) -> Dict[str, Any]: + """ + 导出提示词配置 + + Args: + category: 分类过滤(可选) + + Returns: + 提示词配置数据 + """ + instance = cls() + categories = [category] if category else instance._registry.list_categories() + + export_data = { + "version": "1.0.0", + "exported_at": instance._get_current_time(), + "categories": {} + } + + for cat in categories: + export_data["categories"][cat] = {} + prompts = instance._registry.list_prompts(cat) + + for prompt_name in prompts: + versions = instance._registry.list_versions(cat, prompt_name) + export_data["categories"][cat][prompt_name] = {} + + for ver in versions: + prompt_obj = instance._registry.get(cat, prompt_name, ver) + export_data["categories"][cat][prompt_name][ver] = prompt_obj.to_dict() + + return export_data + + def _get_current_time(self) -> str: + """获取当前时间字符串""" + from datetime import datetime + return datetime.now().isoformat() + + +# 便捷函数 +def get_prompt(category: str, name: str, version: str = None, **parameters) -> str: + """获取提示词的便捷函数""" + return PromptManager.get_prompt(category, name, version, parameters) + + +def validate_prompt_output(output: Union[str, Dict], category: str, name: str, version: str = None) -> Any: + """验证提示词输出的便捷函数""" + return PromptManager.validate_output(output, category, name, version) diff --git a/app/services/prompts/registry.py b/app/services/prompts/registry.py new file mode 100644 index 0000000..f30bf9c --- /dev/null +++ b/app/services/prompts/registry.py @@ -0,0 +1,222 @@ +#!/usr/bin/env python +# -*- coding: UTF-8 -*- + +""" +@Project: NarratoAI +@File : registry.py +@Author : AI Assistant +@Date : 2025/1/7 +@Description: 提示词注册机制 +""" + +from typing import Dict, List, Optional, Tuple +from collections import defaultdict +from loguru import logger + +from .base import BasePrompt, ModelType, OutputFormat +from .exceptions import ( + PromptNotFoundError, + PromptRegistrationError, + PromptVersionError +) + + +class PromptRegistry: + """提示词注册表""" + + def __init__(self): + # 存储结构: {category: {name: {version: prompt}}} + self._prompts: Dict[str, Dict[str, Dict[str, BasePrompt]]] = defaultdict( + lambda: defaultdict(dict) + ) + # 默认版本映射: {category: {name: default_version}} + self._default_versions: Dict[str, Dict[str, str]] = defaultdict(dict) + + def register(self, prompt: BasePrompt, is_default: bool = True) -> None: + """ + 注册提示词 + + Args: + prompt: 提示词实例 + is_default: 是否设为默认版本 + """ + category = prompt.category + name = prompt.name + version = prompt.version + + # 检查是否已存在相同版本 + if version in self._prompts[category][name]: + raise PromptRegistrationError( + category=category, + name=name, + reason=f"版本 {version} 已存在" + ) + + # 注册提示词 + self._prompts[category][name][version] = prompt + + # 设置默认版本 + if is_default or name not in self._default_versions[category]: + self._default_versions[category][name] = version + + logger.info(f"已注册提示词: {category}.{name} v{version}") + + def get(self, category: str, name: str, version: Optional[str] = None) -> BasePrompt: + """ + 获取提示词 + + Args: + category: 分类 + name: 名称 + version: 版本,为None时使用默认版本 + + Returns: + 提示词实例 + """ + if category not in self._prompts: + raise PromptNotFoundError(category, name, version) + + if name not in self._prompts[category]: + raise PromptNotFoundError(category, name, version) + + # 确定版本 + if version is None: + if name not in self._default_versions[category]: + raise PromptNotFoundError(category, name, version) + version = self._default_versions[category][name] + + if version not in self._prompts[category][name]: + raise PromptNotFoundError(category, name, version) + + return self._prompts[category][name][version] + + def list_categories(self) -> List[str]: + """列出所有分类""" + return list(self._prompts.keys()) + + def list_prompts(self, category: str) -> List[str]: + """列出指定分类下的所有提示词名称""" + if category not in self._prompts: + return [] + return list(self._prompts[category].keys()) + + def list_versions(self, category: str, name: str) -> List[str]: + """列出指定提示词的所有版本""" + if category not in self._prompts or name not in self._prompts[category]: + return [] + return list(self._prompts[category][name].keys()) + + def get_default_version(self, category: str, name: str) -> Optional[str]: + """获取默认版本""" + return self._default_versions.get(category, {}).get(name) + + def set_default_version(self, category: str, name: str, version: str) -> None: + """设置默认版本""" + if (category not in self._prompts or + name not in self._prompts[category] or + version not in self._prompts[category][name]): + raise PromptVersionError(category, name, version, "版本不存在") + + self._default_versions[category][name] = version + logger.info(f"已设置默认版本: {category}.{name} -> v{version}") + + def exists(self, category: str, name: str, version: Optional[str] = None) -> bool: + """检查提示词是否存在""" + try: + self.get(category, name, version) + return True + except PromptNotFoundError: + return False + + def remove(self, category: str, name: str, version: Optional[str] = None) -> None: + """移除提示词""" + if version is None: + # 移除所有版本 + if category in self._prompts and name in self._prompts[category]: + del self._prompts[category][name] + if name in self._default_versions.get(category, {}): + del self._default_versions[category][name] + logger.info(f"已移除提示词所有版本: {category}.{name}") + else: + # 移除指定版本 + if (category in self._prompts and + name in self._prompts[category] and + version in self._prompts[category][name]): + del self._prompts[category][name][version] + + # 如果移除的是默认版本,需要重新设置默认版本 + if (self._default_versions.get(category, {}).get(name) == version and + self._prompts[category][name]): + # 选择最新版本作为默认版本 + new_default = max(self._prompts[category][name].keys()) + self._default_versions[category][name] = new_default + logger.info(f"默认版本已更新: {category}.{name} -> v{new_default}") + + logger.info(f"已移除提示词版本: {category}.{name} v{version}") + + def search(self, + keyword: str = None, + category: str = None, + model_type: ModelType = None, + output_format: OutputFormat = None) -> List[Tuple[str, str, str]]: + """ + 搜索提示词 + + Args: + keyword: 关键词(在名称和描述中搜索) + category: 分类过滤 + model_type: 模型类型过滤 + output_format: 输出格式过滤 + + Returns: + 匹配的提示词列表 [(category, name, version), ...] + """ + results = [] + + categories = [category] if category else self._prompts.keys() + + for cat in categories: + for name in self._prompts[cat]: + for version, prompt in self._prompts[cat][name].items(): + # 关键词过滤 + if keyword: + if (keyword.lower() not in name.lower() and + keyword.lower() not in prompt.metadata.description.lower()): + continue + + # 模型类型过滤 + if model_type and prompt.metadata.model_type != model_type: + continue + + # 输出格式过滤 + if output_format and prompt.metadata.output_format != output_format: + continue + + results.append((cat, name, version)) + + return results + + def get_stats(self) -> Dict[str, int]: + """获取注册表统计信息""" + total_prompts = 0 + total_versions = 0 + + for category in self._prompts: + for name in self._prompts[category]: + total_prompts += 1 + total_versions += len(self._prompts[category][name]) + + return { + "categories": len(self._prompts), + "prompts": total_prompts, + "versions": total_versions + } + + +# 全局注册表实例 +_global_registry = PromptRegistry() + + +def get_registry() -> PromptRegistry: + """获取全局注册表实例""" + return _global_registry diff --git a/app/services/prompts/short_drama_editing/__init__.py b/app/services/prompts/short_drama_editing/__init__.py new file mode 100644 index 0000000..ce155b6 --- /dev/null +++ b/app/services/prompts/short_drama_editing/__init__.py @@ -0,0 +1,33 @@ +#!/usr/bin/env python +# -*- coding: UTF-8 -*- + +""" +@Project: NarratoAI +@File : __init__.py +@Author : AI Assistant +@Date : 2025/1/7 +@Description: 短剧混剪提示词模块 +""" + +from .subtitle_analysis import SubtitleAnalysisPrompt +from .plot_extraction import PlotExtractionPrompt +from ..manager import PromptManager + + +def register_prompts(): + """注册短剧混剪相关的提示词""" + + # 注册字幕分析提示词 + subtitle_analysis_prompt = SubtitleAnalysisPrompt() + PromptManager.register_prompt(subtitle_analysis_prompt, is_default=True) + + # 注册爆点提取提示词 + plot_extraction_prompt = PlotExtractionPrompt() + PromptManager.register_prompt(plot_extraction_prompt, is_default=True) + + +__all__ = [ + "SubtitleAnalysisPrompt", + "PlotExtractionPrompt", + "register_prompts" +] diff --git a/app/services/prompts/short_drama_editing/plot_extraction.py b/app/services/prompts/short_drama_editing/plot_extraction.py new file mode 100644 index 0000000..f68d21a --- /dev/null +++ b/app/services/prompts/short_drama_editing/plot_extraction.py @@ -0,0 +1,70 @@ +#!/usr/bin/env python +# -*- coding: UTF-8 -*- + +""" +@Project: NarratoAI +@File : plot_extraction.py +@Author : AI Assistant +@Date : 2025/1/7 +@Description: 短剧爆点提取提示词 +""" + +from ..base import TextPrompt, PromptMetadata, ModelType, OutputFormat + + +class PlotExtractionPrompt(TextPrompt): + """短剧爆点提取提示词""" + + def __init__(self): + metadata = PromptMetadata( + name="plot_extraction", + category="short_drama_editing", + version="v1.0", + description="根据剧情梗概和字幕内容,精确定位关键剧情的时间段", + model_type=ModelType.TEXT, + output_format=OutputFormat.JSON, + tags=["短剧", "爆点定位", "时间戳", "剧情提取"], + parameters=["subtitle_content", "plot_summary", "plot_titles"] + ) + super().__init__(metadata) + + self._system_prompt = "你是一名短剧编剧,非常擅长根据字幕中分析视频中关键剧情出现的具体时间段。" + + def get_template(self) -> str: + return """请仔细阅读剧情梗概和爆点内容,然后在字幕中找出每个爆点发生的具体时间段和爆点前后的详细剧情。 + +剧情梗概: +${plot_summary} + +需要定位的爆点内容: +${plot_titles} + +字幕内容: +${subtitle_content} + +分析要求: +1. 为每个爆点找到对应的具体时间段 +2. 时间段要准确反映该爆点的完整发展过程 +3. 提供爆点前后的详细剧情描述 +4. 确保时间戳格式正确且存在于字幕中 +5. 选择最具戏剧张力的时间段 + +请返回一个JSON对象,包含一个名为"plot_points"的数组,数组中包含多个对象,每个对象都要包含以下字段: + +{{ + "plot_points": [ + {{ + "timestamp": "时间段,格式为xx:xx:xx,xxx-xx:xx:xx,xxx", + "title": "关键剧情的主题", + "picture": "关键剧情前后的详细剧情描述,包括人物对话、动作、情感变化等" + }} + ] +}} + +重要要求: +1. 请确保返回的是合法的JSON格式 +2. 时间戳必须严格按照字幕中的格式 +3. 剧情描述要详细具体,包含关键对话和动作 +4. 每个爆点的时间段要合理,不能过短或过长 +5. 严禁虚构不存在的时间戳或剧情内容 +6. 只输出JSON内容,不要添加任何说明文字""" diff --git a/app/services/prompts/short_drama_editing/subtitle_analysis.py b/app/services/prompts/short_drama_editing/subtitle_analysis.py new file mode 100644 index 0000000..8fc2c28 --- /dev/null +++ b/app/services/prompts/short_drama_editing/subtitle_analysis.py @@ -0,0 +1,68 @@ +#!/usr/bin/env python +# -*- coding: UTF-8 -*- + +""" +@Project: NarratoAI +@File : subtitle_analysis.py +@Author : AI Assistant +@Date : 2025/1/7 +@Description: 短剧字幕分析提示词 +""" + +from ..base import TextPrompt, PromptMetadata, ModelType, OutputFormat + + +class SubtitleAnalysisPrompt(TextPrompt): + """短剧字幕分析提示词""" + + def __init__(self): + metadata = PromptMetadata( + name="subtitle_analysis", + category="short_drama_editing", + version="v1.0", + description="分析短剧字幕内容,提取剧情梗概和关键情节点", + model_type=ModelType.TEXT, + output_format=OutputFormat.JSON, + tags=["短剧", "字幕分析", "剧情梗概", "情节提取"], + parameters=["subtitle_content", "custom_clips"] + ) + super().__init__(metadata) + + self._system_prompt = "你是一名短剧编剧和内容分析师,擅长从字幕中提取剧情要点和关键情节。" + + def get_template(self) -> str: + return """请仔细分析以下短剧字幕内容,提取剧情梗概和关键情节点。 + +字幕内容: +${subtitle_content} + +分析要求: +1. 提取整体剧情梗概,概括主要故事线和核心冲突 +2. 识别 ${custom_clips} 个最具吸引力的关键情节点(爆点) +3. 每个情节点要包含具体的时间段和详细描述 +4. 关注剧情的转折点、冲突高潮、情感爆发等关键时刻 +5. 确保选择的情节点具有强烈的戏剧张力和观看价值 + +请按照以下JSON格式输出分析结果: + +{{ + "summary": "整体剧情梗概,简要概括主要故事线、角色关系和核心冲突", + "plot_titles": [ + "情节点1标题", + "情节点2标题", + "情节点3标题" + ], + "analysis_details": {{ + "main_characters": ["主要角色1", "主要角色2"], + "story_theme": "故事主题", + "conflict_type": "冲突类型(如:爱情、复仇、家庭等)", + "emotional_peaks": ["情感高潮点1", "情感高潮点2"] + }} +}} + +重要要求: +1. 必须输出有效的JSON格式,不能包含注释或其他文字 +2. 剧情梗概要简洁明了,突出核心看点 +3. 情节点标题要吸引人,体现戏剧冲突 +4. 严禁虚构不存在的剧情内容 +5. 分析要客观准确,基于字幕实际内容""" diff --git a/app/services/prompts/short_drama_narration/__init__.py b/app/services/prompts/short_drama_narration/__init__.py new file mode 100644 index 0000000..49aafbf --- /dev/null +++ b/app/services/prompts/short_drama_narration/__init__.py @@ -0,0 +1,33 @@ +#!/usr/bin/env python +# -*- coding: UTF-8 -*- + +""" +@Project: NarratoAI +@File : __init__.py +@Author : AI Assistant +@Date : 2025/1/7 +@Description: 短剧解说提示词模块 +""" + +from .plot_analysis import PlotAnalysisPrompt +from .script_generation import ScriptGenerationPrompt +from ..manager import PromptManager + + +def register_prompts(): + """注册短剧解说相关的提示词""" + + # 注册剧情分析提示词 + plot_analysis_prompt = PlotAnalysisPrompt() + PromptManager.register_prompt(plot_analysis_prompt, is_default=True) + + # 注册解说脚本生成提示词 + script_generation_prompt = ScriptGenerationPrompt() + PromptManager.register_prompt(script_generation_prompt, is_default=True) + + +__all__ = [ + "PlotAnalysisPrompt", + "ScriptGenerationPrompt", + "register_prompts" +] diff --git a/app/services/prompts/short_drama_narration/plot_analysis.py b/app/services/prompts/short_drama_narration/plot_analysis.py new file mode 100644 index 0000000..01663d1 --- /dev/null +++ b/app/services/prompts/short_drama_narration/plot_analysis.py @@ -0,0 +1,90 @@ +#!/usr/bin/env python +# -*- coding: UTF-8 -*- + +""" +@Project: NarratoAI +@File : plot_analysis.py +@Author : AI Assistant +@Date : 2025/1/7 +@Description: 短剧剧情分析提示词 +""" + +from ..base import TextPrompt, PromptMetadata, ModelType, OutputFormat + + +class PlotAnalysisPrompt(TextPrompt): + """短剧剧情分析提示词""" + + def __init__(self): + metadata = PromptMetadata( + name="plot_analysis", + category="short_drama_narration", + version="v1.0", + description="分析短剧字幕内容,提供详细的剧情分析和分段解析", + model_type=ModelType.TEXT, + output_format=OutputFormat.TEXT, + tags=["短剧", "剧情分析", "字幕解析", "分段分析"], + parameters=["subtitle_content"] + ) + super().__init__(metadata) + + self._system_prompt = "你是一位专业的剧本分析师和剧情概括助手。" + + def get_template(self) -> str: + return """# 角色 +你是一位专业的剧本分析师和剧情概括助手。 + +# 任务 +我将为你提供一部短剧的完整字幕文本。请你基于这些字幕,完成以下任务: +1. **整体剧情分析**:简要概括整个短剧的核心剧情脉络、主要冲突和结局(如果有的话)。 +2. **分段剧情解析与时间戳定位**: + * 将整个短剧划分为若干个关键的剧情段落(例如:开端、发展、转折、高潮、结局,或根据具体情节自然划分)。 + * 段落数应该与字幕长度成正比。 + * 对于每一个剧情段落: + * **概括该段落的主要内容**:用简洁的语言描述这段剧情发生了什么。 + * **标注对应的时间戳范围**:明确指出该剧情段落对应的开始字幕时间戳和结束字幕时间戳。请直接从字幕中提取时间信息。 + +# 输入格式 +字幕内容通常包含时间戳和对话,例如: +``` +00:00:05,000 --> 00:00:10,000 +[角色A]: 你好吗? +00:00:10,500 --> 00:00:15,000 +[角色B]: 我很好,谢谢。发生了一些有趣的事情。 +... (更多字幕内容) ... +``` +我将把实际字幕粘贴在下方。 + +# 输出格式要求 +请按照以下格式清晰地呈现分析结果: + +**一、整体剧情概括:** +[此处填写对整个短剧剧情的概括] + +**二、分段剧情解析:** + +**剧情段落 1:[段落主题/概括,例如:主角登场与背景介绍]** +* **时间戳:** [开始时间戳] --> [结束时间戳] +* **内容概要:** [对这段剧情的详细描述] + +**剧情段落 2:[段落主题/概括,例如:第一个冲突出现]** +* **时间戳:** [开始时间戳] --> [结束时间戳] +* **内容概要:** [对这段剧情的详细描述] + +... (根据实际剧情段落数量继续) ... + +**剧情段落 N:[段落主题/概括,例如:结局与反思]** +* **时间戳:** [开始时间戳] --> [结束时间戳] +* **内容概要:** [对这段剧情的详细描述] + +# 注意事项 +* 请确保时间戳的准确性,直接引用字幕中的时间。 +* 剧情段落的划分应合乎逻辑,能够反映剧情的起承转合。 +* 语言表达应简洁、准确、客观。 + +# 限制 +1. 严禁输出与分析结果无关的内容 +2. 时间戳必须严格按照字幕中的实际时间 + +# 请处理以下字幕: +${subtitle_content}""" diff --git a/app/services/prompts/short_drama_narration/script_generation.py b/app/services/prompts/short_drama_narration/script_generation.py new file mode 100644 index 0000000..0bfec6a --- /dev/null +++ b/app/services/prompts/short_drama_narration/script_generation.py @@ -0,0 +1,64 @@ +#!/usr/bin/env python +# -*- coding: UTF-8 -*- + +""" +@Project: NarratoAI +@File : script_generation.py +@Author : AI Assistant +@Date : 2025/1/7 +@Description: 短剧解说脚本生成提示词 +""" + +from ..base import ParameterizedPrompt, PromptMetadata, ModelType, OutputFormat + + +class ScriptGenerationPrompt(ParameterizedPrompt): + """短剧解说脚本生成提示词""" + + def __init__(self): + metadata = PromptMetadata( + name="script_generation", + category="short_drama_narration", + version="v1.0", + description="根据剧情分析生成短剧解说脚本,包含解说文案和原声片段", + model_type=ModelType.TEXT, + output_format=OutputFormat.JSON, + tags=["短剧", "解说脚本", "文案生成", "原声片段"], + parameters=["drama_name", "plot_analysis"] + ) + super().__init__(metadata, required_parameters=["drama_name", "plot_analysis"]) + + self._system_prompt = "你是一位专业的短视频解说脚本撰写专家。你必须严格按照JSON格式输出,不能包含任何其他文字、说明或代码块标记。" + + def get_template(self) -> str: + return """我是一个影视解说up主,需要为我的粉丝讲解短剧《${drama_name}》的剧情,目前正在解说剧情,希望能让粉丝通过我的解说了解剧情,并且产生继续观看的兴趣,请生成一篇解说脚本,包含解说文案,以及穿插原声的片段,下面中的内容是短剧的剧情概述: + + +${plot_analysis} + + +请严格按照以下JSON格式输出,不要添加任何其他文字、说明或代码块标记: + +{{ + "items": [ + {{ + "_id": 1, + "timestamp": "00:00:05,390-00:00:10,430", + "picture": "剧情描述或者备注", + "narration": "解说文案,如果片段为穿插的原片片段,可以直接使用 '播放原片+_id' 进行占位", + "OST": 0 + }} + ] +}} + +重要要求: +1. 必须输出有效的JSON格式,不能包含注释 +2. OST字段必须是数字:0表示解说片段,1表示原片片段 +3. _id必须是递增的数字 +4. 只输出JSON内容,不要输出任何说明文字 +5. 不要使用代码块标记(如```json) +6. 解说文案使用简体中文 +7. 严禁虚构剧情,所有内容只能从中摘取 +8. 严禁虚构时间戳,所有时间戳只能从中摘取 +9. 解说文案要生动有趣,能够吸引观众继续观看 +10. 合理安排解说片段和原片片段的比例,保持节奏感""" diff --git a/app/services/prompts/template.py b/app/services/prompts/template.py new file mode 100644 index 0000000..8ce02f2 --- /dev/null +++ b/app/services/prompts/template.py @@ -0,0 +1,177 @@ +#!/usr/bin/env python +# -*- coding: UTF-8 -*- + +""" +@Project: NarratoAI +@File : template.py +@Author : AI Assistant +@Date : 2025/1/7 +@Description: 模板渲染引擎 +""" + +import re +from typing import Dict, Any, List, Optional +from string import Template +from loguru import logger + +from .exceptions import TemplateRenderError + + +class TemplateRenderer: + """模板渲染器""" + + def __init__(self): + self._custom_filters = {} + + def register_filter(self, name: str, func: callable) -> None: + """注册自定义过滤器""" + self._custom_filters[name] = func + logger.debug(f"已注册模板过滤器: {name}") + + def render(self, template: str, parameters: Dict[str, Any] = None) -> str: + """ + 渲染模板 + + Args: + template: 模板字符串 + parameters: 参数字典 + + Returns: + 渲染后的字符串 + """ + parameters = parameters or {} + + try: + # 使用Python内置的Template类进行基础渲染 + tmpl = Template(template) + + # 先进行基础参数替换 + rendered = tmpl.safe_substitute(**parameters) + + # 处理自定义过滤器 + rendered = self._apply_filters(rendered, parameters) + + return rendered + + except Exception as e: + raise TemplateRenderError( + template_name="unknown", + error_message=f"模板渲染失败: {str(e)}" + ) + + def _apply_filters(self, text: str, parameters: Dict[str, Any]) -> str: + """应用自定义过滤器""" + # 查找过滤器模式: ${variable|filter_name} + filter_pattern = r'\$\{([^}]+)\|([^}]+)\}' + + def replace_filter(match): + var_name = match.group(1).strip() + filter_name = match.group(2).strip() + + if filter_name not in self._custom_filters: + logger.warning(f"未知的过滤器: {filter_name}") + return match.group(0) # 返回原始文本 + + if var_name not in parameters: + logger.warning(f"参数不存在: {var_name}") + return match.group(0) # 返回原始文本 + + try: + filter_func = self._custom_filters[filter_name] + filtered_value = filter_func(parameters[var_name]) + return str(filtered_value) + except Exception as e: + logger.error(f"过滤器执行失败 {filter_name}: {str(e)}") + return match.group(0) # 返回原始文本 + + return re.sub(filter_pattern, replace_filter, text) + + def extract_variables(self, template: str) -> List[str]: + """提取模板中的变量名""" + # 匹配 ${variable} 和 ${variable|filter} 模式 + pattern = r'\$\{([^}|]+)(?:\|[^}]+)?\}' + matches = re.findall(pattern, template) + return list(set(match.strip() for match in matches)) + + def validate_template(self, template: str, required_params: List[str] = None) -> bool: + """验证模板""" + try: + # 提取模板变量 + template_vars = self.extract_variables(template) + + # 检查必需参数 + if required_params: + missing_params = set(required_params) - set(template_vars) + if missing_params: + raise TemplateRenderError( + template_name="validation", + error_message="模板缺少必需参数", + missing_params=list(missing_params) + ) + + # 尝试渲染测试 + test_params = {var: f"test_{var}" for var in template_vars} + self.render(template, test_params) + + return True + + except Exception as e: + logger.error(f"模板验证失败: {str(e)}") + return False + + +# 内置过滤器 +def _upper_filter(value: Any) -> str: + """转换为大写""" + return str(value).upper() + + +def _lower_filter(value: Any) -> str: + """转换为小写""" + return str(value).lower() + + +def _title_filter(value: Any) -> str: + """转换为标题格式""" + return str(value).title() + + +def _strip_filter(value: Any) -> str: + """去除首尾空白""" + return str(value).strip() + + +def _truncate_filter(value: Any, length: int = 100) -> str: + """截断文本""" + text = str(value) + if len(text) <= length: + return text + return text[:length] + "..." + + +def _json_filter(value: Any) -> str: + """转换为JSON字符串""" + import json + return json.dumps(value, ensure_ascii=False, indent=2) + + +# 全局渲染器实例 +_global_renderer = TemplateRenderer() + +# 注册内置过滤器 +_global_renderer.register_filter("upper", _upper_filter) +_global_renderer.register_filter("lower", _lower_filter) +_global_renderer.register_filter("title", _title_filter) +_global_renderer.register_filter("strip", _strip_filter) +_global_renderer.register_filter("truncate", _truncate_filter) +_global_renderer.register_filter("json", _json_filter) + + +def get_renderer() -> TemplateRenderer: + """获取全局渲染器实例""" + return _global_renderer + + +def render_template(template: str, parameters: Dict[str, Any] = None) -> str: + """便捷的模板渲染函数""" + return _global_renderer.render(template, parameters) diff --git a/app/services/prompts/validators.py b/app/services/prompts/validators.py new file mode 100644 index 0000000..d8a1651 --- /dev/null +++ b/app/services/prompts/validators.py @@ -0,0 +1,250 @@ +#!/usr/bin/env python +# -*- coding: UTF-8 -*- + +""" +@Project: NarratoAI +@File : validators.py +@Author : AI Assistant +@Date : 2025/1/7 +@Description: 提示词输出验证器 +""" + +import json +import re +from typing import Dict, Any, List, Optional, Union +from loguru import logger + +from .base import OutputFormat +from .exceptions import PromptValidationError + + +class PromptOutputValidator: + """提示词输出验证器""" + + @staticmethod + def validate_json(output: str, schema: Dict[str, Any] = None) -> Dict[str, Any]: + """ + 验证JSON输出 + + Args: + output: 输出字符串 + schema: JSON schema(可选) + + Returns: + 解析后的JSON对象 + """ + try: + # 清理输出(移除可能的代码块标记) + cleaned_output = PromptOutputValidator._clean_json_output(output) + + # 解析JSON + parsed = json.loads(cleaned_output) + + # Schema验证(如果提供) + if schema: + PromptOutputValidator._validate_json_schema(parsed, schema) + + return parsed + + except json.JSONDecodeError as e: + raise PromptValidationError(f"JSON格式错误: {str(e)}") + except Exception as e: + raise PromptValidationError(f"JSON验证失败: {str(e)}") + + @staticmethod + def validate_narration_script(output: Union[str, Dict]) -> Dict[str, Any]: + """ + 验证解说文案输出格式 + + Args: + output: 输出内容(字符串或字典) + + Returns: + 验证后的解说文案数据 + """ + # 如果是字符串,先解析为JSON + if isinstance(output, str): + data = PromptOutputValidator.validate_json(output) + else: + data = output + + # 验证必需字段 + if "items" not in data: + raise PromptValidationError("解说文案缺少 'items' 字段") + + items = data["items"] + if not isinstance(items, list): + raise PromptValidationError("'items' 字段必须是数组") + + if not items: + raise PromptValidationError("解说文案不能为空") + + # 验证每个item + for i, item in enumerate(items): + PromptOutputValidator._validate_narration_item(item, i) + + logger.debug(f"解说文案验证通过,包含 {len(items)} 个片段") + return data + + @staticmethod + def validate_plot_analysis(output: Union[str, Dict]) -> Dict[str, Any]: + """ + 验证剧情分析输出格式 + + Args: + output: 输出内容 + + Returns: + 验证后的剧情分析数据 + """ + if isinstance(output, str): + data = PromptOutputValidator.validate_json(output) + else: + data = output + + # 验证剧情分析必需字段 + required_fields = ["summary", "plot_points"] + for field in required_fields: + if field not in data: + raise PromptValidationError(f"剧情分析缺少 '{field}' 字段") + + # 验证plot_points + plot_points = data["plot_points"] + if not isinstance(plot_points, list): + raise PromptValidationError("'plot_points' 字段必须是数组") + + for i, point in enumerate(plot_points): + PromptOutputValidator._validate_plot_point(point, i) + + logger.debug(f"剧情分析验证通过,包含 {len(plot_points)} 个情节点") + return data + + @staticmethod + def _clean_json_output(output: str) -> str: + """清理JSON输出""" + # 移除可能的代码块标记 + output = re.sub(r'^```json\s*', '', output, flags=re.MULTILINE) + output = re.sub(r'^```\s*$', '', output, flags=re.MULTILINE) + + # 移除前后空白 + output = output.strip() + + # 尝试提取JSON部分(如果有其他文本) + json_match = re.search(r'\{.*\}', output, re.DOTALL) + if json_match: + output = json_match.group(0) + + return output + + @staticmethod + def _validate_json_schema(data: Dict[str, Any], schema: Dict[str, Any]) -> None: + """验证JSON Schema""" + # 简单的schema验证实现 + for field, field_type in schema.items(): + if field not in data: + raise PromptValidationError(f"缺少必需字段: {field}") + + if not isinstance(data[field], field_type): + raise PromptValidationError( + f"字段 '{field}' 类型错误,期望: {field_type.__name__},实际: {type(data[field]).__name__}" + ) + + @staticmethod + def _validate_narration_item(item: Dict[str, Any], index: int) -> None: + """验证解说文案项目""" + required_fields = ["_id", "timestamp", "picture", "narration"] + + for field in required_fields: + if field not in item: + raise PromptValidationError(f"第 {index + 1} 个片段缺少 '{field}' 字段") + + # 验证_id + if not isinstance(item["_id"], int) or item["_id"] <= 0: + raise PromptValidationError(f"第 {index + 1} 个片段的 '_id' 必须是正整数") + + # 验证timestamp格式 + timestamp = item["timestamp"] + if not isinstance(timestamp, str): + raise PromptValidationError(f"第 {index + 1} 个片段的 'timestamp' 必须是字符串") + + # 验证时间戳格式 (HH:MM:SS,mmm-HH:MM:SS,mmm) + timestamp_pattern = r'^\d{2}:\d{2}:\d{2},\d{3}-\d{2}:\d{2}:\d{2},\d{3}$' + if not re.match(timestamp_pattern, timestamp): + raise PromptValidationError( + f"第 {index + 1} 个片段的时间戳格式错误,应为 'HH:MM:SS,mmm-HH:MM:SS,mmm'" + ) + + # 验证文本字段不为空 + for field in ["picture", "narration"]: + if not isinstance(item[field], str) or not item[field].strip(): + raise PromptValidationError(f"第 {index + 1} 个片段的 '{field}' 不能为空") + + # 验证OST字段(如果存在) + if "OST" in item: + if not isinstance(item["OST"], int) or item["OST"] not in [0, 1, 2]: + raise PromptValidationError( + f"第 {index + 1} 个片段的 'OST' 必须是 0、1 或 2" + ) + + @staticmethod + def _validate_plot_point(point: Dict[str, Any], index: int) -> None: + """验证剧情点""" + required_fields = ["timestamp", "title", "picture"] + + for field in required_fields: + if field not in point: + raise PromptValidationError(f"第 {index + 1} 个剧情点缺少 '{field}' 字段") + + # 验证字段类型和内容 + for field in required_fields: + if not isinstance(point[field], str) or not point[field].strip(): + raise PromptValidationError(f"第 {index + 1} 个剧情点的 '{field}' 不能为空") + + # 验证时间戳格式 + timestamp = point["timestamp"] + # 支持多种时间戳格式 + patterns = [ + r'^\d{2}:\d{2}:\d{2},\d{3}-\d{2}:\d{2}:\d{2},\d{3}$', # HH:MM:SS,mmm-HH:MM:SS,mmm + r'^\d{2}:\d{2}:\d{2}-\d{2}:\d{2}:\d{2}$', # HH:MM:SS-HH:MM:SS + ] + + if not any(re.match(pattern, timestamp) for pattern in patterns): + raise PromptValidationError( + f"第 {index + 1} 个剧情点的时间戳格式错误" + ) + + @staticmethod + def validate_by_format(output: str, format_type: OutputFormat, schema: Dict[str, Any] = None) -> Any: + """ + 根据格式类型验证输出 + + Args: + output: 输出内容 + format_type: 输出格式类型 + schema: 验证schema(可选) + + Returns: + 验证后的数据 + """ + if format_type == OutputFormat.JSON: + return PromptOutputValidator.validate_json(output, schema) + elif format_type == OutputFormat.TEXT: + return output.strip() + elif format_type == OutputFormat.MARKDOWN: + return output.strip() + elif format_type == OutputFormat.STRUCTURED: + # 结构化数据需要根据具体类型处理 + return PromptOutputValidator.validate_json(output, schema) + else: + raise PromptValidationError(f"不支持的输出格式: {format_type}") + + +# 便捷函数 +def validate_json_output(output: str, schema: Dict[str, Any] = None) -> Dict[str, Any]: + """验证JSON输出的便捷函数""" + return PromptOutputValidator.validate_json(output, schema) + + +def validate_narration_output(output: Union[str, Dict]) -> Dict[str, Any]: + """验证解说文案输出的便捷函数""" + return PromptOutputValidator.validate_narration_script(output) diff --git a/docs/prompt_management_system.md b/docs/prompt_management_system.md new file mode 100644 index 0000000..74abac2 --- /dev/null +++ b/docs/prompt_management_system.md @@ -0,0 +1,221 @@ +# 提示词管理系统文档 + +## 概述 + +本项目实现了统一的提示词管理系统,用于集中管理三个核心功能的提示词: +- **纪录片解说** - 视频帧分析和解说文案生成 +- **短剧混剪** - 字幕分析和爆点提取 +- **短剧解说** - 剧情分析和解说脚本生成 + +## 系统架构 + +``` +app/services/prompts/ +├── __init__.py # 模块初始化 +├── base.py # 基础提示词类 +├── manager.py # 提示词管理器 +├── registry.py # 提示词注册机制 +├── template.py # 模板渲染引擎 +├── validators.py # 输出验证器 +├── exceptions.py # 异常定义 +├── documentary/ # 纪录片解说提示词 +│ ├── __init__.py +│ ├── frame_analysis.py # 视频帧分析 +│ └── narration_generation.py # 解说文案生成 +├── short_drama_editing/ # 短剧混剪提示词 +│ ├── __init__.py +│ ├── subtitle_analysis.py # 字幕分析 +│ └── plot_extraction.py # 爆点提取 +└── short_drama_narration/ # 短剧解说提示词 + ├── __init__.py + ├── plot_analysis.py # 剧情分析 + └── script_generation.py # 解说脚本生成 +``` + +## 核心特性 + +### 1. 统一管理 +- 所有提示词集中在 `app/services/prompts/` 模块中 +- 按功能模块分类组织 +- 支持版本控制和回滚 + +### 2. 模型类型适配 +- **TextPrompt**: 文本模型专用 +- **VisionPrompt**: 视觉模型专用 +- **ParameterizedPrompt**: 支持参数化 + +### 3. 参数化支持 +- 动态参数替换 +- 参数验证 +- 模板渲染 + +### 4. 输出验证 +- 严格的JSON格式验证 +- 特定业务场景验证(解说文案、剧情分析等) +- 自定义验证规则 + +## 使用方法 + +### 基本用法 + +```python +from app.services.prompts import PromptManager + +# 获取纪录片解说的视频帧分析提示词 +prompt = PromptManager.get_prompt( + category="documentary", + name="frame_analysis", + parameters={ + "video_theme": "荒野建造", + "custom_instructions": "请特别关注建造过程的细节" + } +) + +# 获取短剧解说的剧情分析提示词 +prompt = PromptManager.get_prompt( + category="short_drama_narration", + name="plot_analysis", + parameters={"subtitle_content": "字幕内容..."} +) +``` + +### 高级功能 + +```python +# 搜索提示词 +results = PromptManager.search_prompts( + keyword="分析", + model_type=ModelType.TEXT +) + +# 获取提示词详细信息 +info = PromptManager.get_prompt_info( + category="documentary", + name="narration_generation" +) + +# 验证输出 +validated_data = PromptManager.validate_output( + output=llm_response, + category="documentary", + name="narration_generation" +) +``` + +## 已注册的提示词 + +### 纪录片解说 (documentary) +- `frame_analysis` - 视频帧分析提示词 +- `narration_generation` - 解说文案生成提示词 + +### 短剧混剪 (short_drama_editing) +- `subtitle_analysis` - 字幕分析提示词 +- `plot_extraction` - 爆点提取提示词 + +### 短剧解说 (short_drama_narration) +- `plot_analysis` - 剧情分析提示词 +- `script_generation` - 解说脚本生成提示词 + +## 迁移指南 + +### 旧代码迁移 + +**之前的用法:** +```python +from app.services.SDE.prompt import subtitle_plot_analysis_v1 +prompt = subtitle_plot_analysis_v1 +``` + +**新的用法:** +```python +from app.services.prompts import PromptManager +prompt = PromptManager.get_prompt( + category="short_drama_narration", + name="plot_analysis", + parameters={"subtitle_content": content} +) +``` + +### 已更新的文件 +- `app/services/SDE/short_drama_explanation.py` +- `app/services/SDP/utils/step1_subtitle_analyzer_openai.py` +- `app/services/generate_narration_script.py` + +## 扩展指南 + +### 添加新提示词 + +1. 在相应分类目录下创建新的提示词类: + +```python +from ..base import TextPrompt, PromptMetadata, ModelType, OutputFormat + +class NewPrompt(TextPrompt): + def __init__(self): + metadata = PromptMetadata( + name="new_prompt", + category="your_category", + version="v1.0", + description="提示词描述", + model_type=ModelType.TEXT, + output_format=OutputFormat.JSON, + parameters=["param1", "param2"] + ) + super().__init__(metadata) + + def get_template(self) -> str: + return "您的提示词模板内容..." +``` + +2. 在 `__init__.py` 中注册: + +```python +def register_prompts(): + new_prompt = NewPrompt() + PromptManager.register_prompt(new_prompt, is_default=True) +``` + +### 添加新分类 + +1. 创建新的分类目录 +2. 实现提示词类 +3. 在主模块的 `__init__.py` 中导入并注册 + +## 测试 + +运行测试脚本验证系统功能: + +```bash +python test_prompt_system.py +``` + +## 注意事项 + +1. **模板参数**: 使用 `${parameter_name}` 格式 +2. **JSON转义**: 模板中的JSON需要使用双大括号 `{{` 和 `}}` +3. **参数验证**: 必需参数会自动验证 +4. **版本管理**: 支持多版本共存,默认使用最新版本 +5. **输出验证**: 建议对LLM输出进行验证以确保格式正确 + +## 性能优化 + +- 提示词模板会被缓存 +- 支持批量操作 +- 异步渲染支持(未来版本) + +## 故障排除 + +### 常见问题 + +1. **模板渲染错误**: 检查参数名称和格式 +2. **提示词未找到**: 确认分类、名称和版本正确 +3. **输出验证失败**: 检查LLM输出格式是否符合要求 + +### 日志调试 + +系统使用 loguru 记录详细日志,可通过日志排查问题: + +```python +from loguru import logger +logger.debug("调试信息") +```