342 lines
8.5 KiB
Python
342 lines
8.5 KiB
Python
"""
|
||
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"
|
||
_SYSTEM_META_PATH = SYSTEM_PROMPTS_DIR / "_meta.json"
|
||
|
||
|
||
def _load_system_meta() -> dict:
|
||
"""读取 system/_meta.json"""
|
||
if _SYSTEM_META_PATH.exists():
|
||
try:
|
||
return json.loads(_SYSTEM_META_PATH.read_text(encoding="utf-8"))
|
||
except (json.JSONDecodeError, OSError):
|
||
pass
|
||
return {}
|
||
|
||
|
||
def list_categories() -> list[dict]:
|
||
"""
|
||
返回所有分类结构
|
||
|
||
以 system/_meta.json 为准,只返回配置中定义的大类。
|
||
|
||
Returns:
|
||
[
|
||
{
|
||
"code": "bk",
|
||
"name": "装修避坑"
|
||
},
|
||
...
|
||
]
|
||
"""
|
||
meta = _load_system_meta()
|
||
categories = []
|
||
|
||
for cat_meta in meta.get("categories", []):
|
||
cat_code = cat_meta["code"]
|
||
cat_name = cat_meta.get("name", cat_code)
|
||
categories.append(
|
||
{
|
||
"code": cat_code,
|
||
"name": cat_name,
|
||
}
|
||
)
|
||
|
||
return categories
|
||
|
||
|
||
def list_prompt_files(category: str) -> list[dict]:
|
||
"""
|
||
扫描指定分类目录下的所有提示词文件
|
||
|
||
文件名格式: 文案——描述.txt
|
||
解析为 label + desc 返回给前端展示。
|
||
|
||
Args:
|
||
category: 大类代码,如 "bk"
|
||
|
||
Returns:
|
||
[
|
||
{
|
||
"filename": "水电改造避坑——水电改造的4个坑.txt",
|
||
"label": "水电改造避坑",
|
||
"desc": "水电改造的4个坑"
|
||
},
|
||
...
|
||
]
|
||
"""
|
||
cat_dir = SYSTEM_PROMPTS_DIR / category
|
||
if not cat_dir.exists():
|
||
return []
|
||
|
||
files = []
|
||
for f in sorted(cat_dir.iterdir()):
|
||
if f.is_file() and f.suffix == ".txt":
|
||
name = f.stem # 不含 .txt
|
||
if "——" in name:
|
||
label, desc = name.split("——", 1)
|
||
else:
|
||
label = name
|
||
desc = ""
|
||
files.append(
|
||
{
|
||
"filename": f.name,
|
||
"label": label.strip(),
|
||
"desc": desc.strip(),
|
||
}
|
||
)
|
||
|
||
return files
|
||
|
||
|
||
def load_prompt_file(category: str, filename: str) -> str:
|
||
"""
|
||
加载指定分类下的指定提示词文件
|
||
|
||
Args:
|
||
category: 大类代码,如 "bk"
|
||
filename: 文件名,如 "水电改造避坑——水电改造的4个坑.txt"
|
||
|
||
Returns:
|
||
提示词内容,文件不存在返回空字符串
|
||
"""
|
||
file_path = SYSTEM_PROMPTS_DIR / category / filename
|
||
if file_path.exists():
|
||
return file_path.read_text(encoding="utf-8")
|
||
return ""
|
||
|
||
|
||
def load_system_prompt(category: str, subcategory: str) -> str:
|
||
"""
|
||
【已废弃】根据大类+小类随机加载一个 System Prompt
|
||
|
||
保留兼容旧调用,实际行为改为从平铺文件中随机加载。
|
||
"""
|
||
files = list_prompt_files(category)
|
||
if not files:
|
||
return ""
|
||
chosen = random.choice(files) # nosec: B311
|
||
return load_prompt_file(category, chosen["filename"])
|
||
|
||
|
||
def load_script_user_prompt(
|
||
topic: str,
|
||
extra_params: str | None = None,
|
||
) -> str:
|
||
"""
|
||
加载并渲染脚本生成 User Prompt
|
||
|
||
Args:
|
||
topic: 创作主题名称
|
||
extra_params: 额外参数(如风格、人设等),以换行分隔的字符串
|
||
|
||
Returns:
|
||
渲染后的用户提示词
|
||
"""
|
||
template = load_prompt("user/script")
|
||
return render_template(
|
||
template,
|
||
topic=topic,
|
||
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
|
||
"""
|
||
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. 控制字数,不要过长
|
||
|
||
请直接返回优化后的文案,不要添加解释。"""
|