Files
meijiaka-zy/python-api/app/ai/prompts/loader.py
T

351 lines
9.3 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
Prompt 简单加载器
=================
从文件加载 Prompt,支持热更新。
目录结构约定:
system/
├── <category>/ # 大类目录
│ ├── <subcategory>/ # 小类目录
│ │ ├── _meta.json # 元数据 {"name": "显示名称"}
│ │ ├── 1.txt # 提示词文件(随机取其一)
│ │ └── 2.txt
│ └── ...
└── ...
"""
import json
import random
from pathlib import Path
from string import Template
_PROMPTS_DIR = Path(__file__).parent
def load_prompt(path: str) -> str:
"""
加载 Prompt 文件
Args:
path: 相对路径,如 "script/system", "polish/scene"
Returns:
Prompt 内容,文件不存在返回空字符串
"""
file_path = _PROMPTS_DIR / f"{path}.txt"
if file_path.exists():
return file_path.read_text(encoding="utf-8")
return ""
def render_template(template: str, **kwargs) -> str:
"""
安全渲染模板变量
Args:
template: 模板字符串
**kwargs: 变量值
Returns:
渲染后的字符串
"""
try:
# 转义 $ 符号防止用户输入干扰
safe_kwargs = {k: str(v).replace("$", "$$") for k, v in kwargs.items()}
return Template(template).substitute(**safe_kwargs)
except KeyError as e:
raise ValueError(f"模板缺少变量: {e}")
# ====================== 新分类体系:动态扫描 ======================
SYSTEM_PROMPTS_DIR = _PROMPTS_DIR / "system"
def list_categories() -> list[dict]:
"""
扫描 system/ 目录,返回所有分类结构
Returns:
[
{
"code": "bk",
"name": "装修避坑",
"subcategories": [
{"code": "ht", "name": "装修合同避坑", "count": 2},
...
]
},
...
]
"""
categories = []
if not SYSTEM_PROMPTS_DIR.exists():
return categories
for cat_dir in sorted(SYSTEM_PROMPTS_DIR.iterdir()):
if not cat_dir.is_dir():
continue
# 读取大类元数据
cat_meta = _load_meta(cat_dir)
cat_name = cat_meta.get("name", cat_dir.name)
subcategories = []
for sub_dir in sorted(cat_dir.iterdir()):
if not sub_dir.is_dir():
continue
# 读取小类元数据
sub_meta = _load_meta(sub_dir)
sub_name = sub_meta.get("name", sub_dir.name)
# 统计提示词文件数量(排除 _meta.json)
prompt_files = [
f for f in sub_dir.iterdir()
if f.is_file() and f.name != "_meta.json"
]
subcategories.append({
"code": sub_dir.name,
"name": sub_name,
"count": len(prompt_files),
})
if subcategories:
categories.append({
"code": cat_dir.name,
"name": cat_name,
"subcategories": subcategories,
})
return categories
def _load_meta(directory: Path) -> dict:
"""读取目录下的 _meta.json"""
meta_path = directory / "_meta.json"
if meta_path.exists():
try:
return json.loads(meta_path.read_text(encoding="utf-8"))
except (json.JSONDecodeError, OSError):
pass
return {}
def load_system_prompt(category: str, subcategory: str) -> str:
"""
根据大类+小类随机加载一个 System Prompt
Args:
category: 大类代码,如 "bk"
subcategory: 小类代码,如 "ht"
Returns:
随机选中的提示词内容,未找到返回空字符串
"""
sub_dir = SYSTEM_PROMPTS_DIR / category / subcategory
if not sub_dir.exists():
return ""
# 收集所有提示词文件(排除 _meta.json)
prompt_files = [
f for f in sub_dir.iterdir()
if f.is_file() and f.suffix == ".txt"
]
if not prompt_files:
return ""
# 随机取一个
chosen = random.choice(prompt_files)
return chosen.read_text(encoding="utf-8")
# ====================== 兼容旧逻辑(废弃) ======================
# 预定义的脚本类型和风格
SCRIPT_TYPES = [
{"id": "干货型", "name": "干货型", "description": "知识分享、技巧传授"},
{"id": "故事型", "name": "故事型", "description": "案例故事、用户体验"},
{"id": "对比型", "name": "对比型", "description": "产品对比、优劣分析"},
{"id": "避坑型", "name": "避坑型", "description": "防骗指南、常见误区"},
{"id": "测评型", "name": "测评型", "description": "产品测评、真实体验"},
]
VIDEO_STYLES = [
{"id": "口播", "name": "口播", "description": "真人出镜讲解"},
{"id": "图文", "name": "图文", "description": "图片+文字+配音"},
{"id": "混剪", "name": "混剪", "description": "素材混剪+配音"},
{"id": "剧情", "name": "剧情", "description": "情景剧演绎"},
{"id": "Vlog", "name": "Vlog", "description": "记录式视频"},
]
def load_script_user_prompt(
topic: str,
duration: int,
extra_params: str | None = None,
) -> str:
"""
加载并渲染脚本生成 User Prompt
Args:
topic: 创作主题名称
duration: 视频时长(秒)
extra_params: 额外参数(如风格、人设等),以换行分隔的字符串
Returns:
渲染后的用户提示词
"""
template = load_prompt("user/script")
return render_template(
template,
topic=topic,
duration=duration,
extra_params=extra_params or "",
)
class ScriptPromptBuilder:
"""
脚本 Prompt 构建器
用于构建家装行业短视频脚本的 System Prompt。
"""
def build(
self,
duration: int = 30,
script_type: str = "干货型",
video_style: str = "口播",
industry: str = "家装",
tone: str | None = None,
custom_requirements: str | None = None,
) -> str:
"""
构建系统 Prompt
Args:
duration: 视频时长(秒)
script_type: 脚本类型(干货型、故事型等)
video_style: 视频风格(口播、剧情等)
industry: 行业(家装)
tone: 语气风格
custom_requirements: 自定义要求
Returns:
完整的 System Prompt
"""
# 基础 System Prompt(已废弃的脚本 system.txt,这里留空)
base_prompt = ""
# 构建上下文信息
context_parts = [
f"行业:{industry}",
f"时长:{duration}",
f"类型:{script_type}",
f"风格:{video_style}",
]
if tone:
context_parts.append(f"语气:{tone}")
context = "\n".join(context_parts)
# 构建完整 Prompt
full_prompt = f"""{base_prompt}
【创作要求】
{context}
"""
if custom_requirements:
full_prompt += f"""
【特殊要求】
{custom_requirements}
"""
# 添加输出格式要求
full_prompt += """
【输出格式】
请严格按照以下 JSON 数组格式返回,每个元素代表一个镜头:
[
{
"id": 1,
"type": "segment",
"scene": "画面描述",
"voiceover": "配音文案",
"duration": "5s"
}
]
type 可以是:
- "segment": 分镜(有画面+配音)
- "empty_shot": 空镜(纯画面,voiceover 可为空)
注意:
1. 只返回 JSON 数组,不要有其他文字
2. 确保 JSON 格式正确
3. 总时长必须严格控制在要求范围内
"""
return full_prompt
class PolishPromptBuilder:
"""
润色 Prompt 构建器
用于构建润色文案或画面描述的 Prompt。
"""
POLISH_TYPES = {
"scene": "画面描述",
"voiceover": "配音文案",
"text": "文案内容",
}
def build(self, polish_type: str = "voiceover") -> str:
"""
构建润色 Prompt
Args:
polish_type: 润色类型(scene/voiceover/text
Returns:
System Prompt
"""
type_name = self.POLISH_TYPES.get(polish_type, "文案")
if polish_type == "scene":
return self._build_scene_prompt()
else:
return self._build_voiceover_prompt()
def _build_scene_prompt(self) -> str:
"""构建画面描述润色 Prompt"""
return """你是一位专业的视频画面描述优化师。你的任务是优化画面描述,使其更加生动、具体、有画面感。
优化要求:
1. 增加细节描写(光线、色彩、构图)
2. 使用专业的影视语言
3. 描述要具体可执行
4. 保持简洁,不要过度渲染
5. 适合 AI 视频生成模型理解
请直接返回优化后的画面描述,不要添加解释。"""
def _build_voiceover_prompt(self) -> str:
"""构建配音文案润色 Prompt"""
return """你是一位专业的短视频文案编辑。你的任务是优化口播文案,使其更加流畅、有吸引力。
优化要求:
1. 语言口语化,适合朗读
2. 增加节奏感和停顿
3. 保留核心信息点
4. 适当使用修辞手法
5. 控制字数,不要过长
请直接返回优化后的文案,不要添加解释。"""