feat: AI改写功能集成角色数据 + UI优化
- 新增story_characters表和seed_characters.sql种子数据(27个角色) - AI改写/续写功能注入角色信息(性别/年龄/外貌/性格) - 首页UI下移避让微信退出按钮 - 个人中心页面布局重构
This commit is contained in:
@@ -7,6 +7,28 @@ import httpx
|
||||
import json
|
||||
import re
|
||||
|
||||
|
||||
def format_characters_prompt(characters: List[Dict]) -> str:
|
||||
"""格式化角色信息为prompt文本"""
|
||||
if not characters:
|
||||
return ""
|
||||
|
||||
text = "\n【故事角色设定】\n"
|
||||
for char in characters:
|
||||
text += f"- {char.get('name', '未知')}({char.get('role_type', '配角')}):"
|
||||
if char.get('gender'):
|
||||
text += f" {char.get('gender')},"
|
||||
if char.get('age_range'):
|
||||
text += f" {char.get('age_range')},"
|
||||
if char.get('appearance'):
|
||||
text += f" 外貌:{char.get('appearance')[:100]},"
|
||||
if char.get('personality'):
|
||||
text += f" 性格:{char.get('personality')[:100]}"
|
||||
text += "\n"
|
||||
|
||||
return text
|
||||
|
||||
|
||||
class AIService:
|
||||
def __init__(self):
|
||||
from app.config import get_settings
|
||||
@@ -49,7 +71,8 @@ class AIService:
|
||||
story_category: str,
|
||||
ending_name: str,
|
||||
ending_content: str,
|
||||
user_prompt: str
|
||||
user_prompt: str,
|
||||
characters: List[Dict] = None
|
||||
) -> Optional[Dict[str, Any]]:
|
||||
"""
|
||||
AI改写结局
|
||||
@@ -58,14 +81,19 @@ class AIService:
|
||||
if not self.enabled or not self.api_key:
|
||||
return None
|
||||
|
||||
# 格式化角色信息
|
||||
characters_text = format_characters_prompt(characters) if characters else ""
|
||||
|
||||
# 构建Prompt
|
||||
system_prompt = """你是一个专业的互动故事创作专家。根据用户的改写指令,重新创作故事结局。
|
||||
system_prompt = f"""你是一个专业的互动故事创作专家。根据用户的改写指令,重新创作故事结局。
|
||||
{characters_text}
|
||||
要求:
|
||||
1. 保持原故事的世界观和人物性格
|
||||
2. 结局要有张力和情感冲击
|
||||
3. 结局内容字数控制在200-400字
|
||||
4. 为新结局取一个4-8字的新名字,体现改写后的剧情走向
|
||||
5. 输出格式必须是JSON:{"ending_name": "新结局名称", "content": "结局内容"}"""
|
||||
5. 如果有角色设定,必须保持角色性格和外貌描述的一致性
|
||||
6. 输出格式必须是JSON:{{"ending_name": "新结局名称", "content": "结局内容"}}"""
|
||||
|
||||
user_prompt_text = f"""故事标题:{story_title}
|
||||
故事分类:{story_category}
|
||||
@@ -97,7 +125,8 @@ class AIService:
|
||||
story_category: str,
|
||||
path_history: List[Dict[str, str]],
|
||||
current_content: str,
|
||||
user_prompt: str
|
||||
user_prompt: str,
|
||||
characters: List[Dict] = None
|
||||
) -> Optional[Dict[str, Any]]:
|
||||
"""
|
||||
AI改写中间章节,生成新的剧情分支
|
||||
@@ -107,12 +136,16 @@ class AIService:
|
||||
print(f"[rewrite_branch] user_prompt={user_prompt}")
|
||||
print(f"[rewrite_branch] path_history长度={len(path_history)}")
|
||||
print(f"[rewrite_branch] current_content长度={len(current_content)}")
|
||||
print(f"[rewrite_branch] characters数量={len(characters) if characters else 0}")
|
||||
print(f"[rewrite_branch] enabled={self.enabled}, api_key存在={bool(self.api_key)}")
|
||||
|
||||
if not self.enabled or not self.api_key:
|
||||
print(f"[rewrite_branch] 服务未启用或API Key为空,返回None")
|
||||
return None
|
||||
|
||||
# 格式化角色信息
|
||||
characters_text = format_characters_prompt(characters) if characters else ""
|
||||
|
||||
# 构建路径历史文本
|
||||
path_text = ""
|
||||
for i, item in enumerate(path_history, 1):
|
||||
@@ -121,19 +154,20 @@ class AIService:
|
||||
path_text += f" → 用户选择:{item['choice']}\n"
|
||||
|
||||
# 构建系统提示词
|
||||
system_prompt = """你是一个专业的互动故事续写专家。用户正在玩一个互动故事,想要在当前位置改变剧情走向。
|
||||
|
||||
system_prompt_header = f"""你是一个专业的互动故事续写专家。用户正在玩一个互动故事,想要在当前位置改变剧情走向。
|
||||
{characters_text}
|
||||
【任务】
|
||||
请从当前节点开始,创作新的剧情分支。新剧情的第一句话必须紧接当前节点最后的情节,仿佛是同一段故事的自然延续,不能有任何跳跃感。
|
||||
|
||||
【写作要求】
|
||||
1. 第一个节点必须紧密衔接当前剧情,像是同一段话的下一句
|
||||
2. 生成 4-6 个新节点,形成有层次的剧情发展(起承转合)
|
||||
3. 每个节点内容 150-300 字,要分 2-3 个自然段(用\n\n分隔),包含:场景描写、人物对话、心理活动
|
||||
3. 每个节点内容 150-300 字,要分 2-3 个自然段(用\\n\\n分隔),包含:场景描写、人物对话、心理活动
|
||||
4. 每个非结局节点有 2 个选项,选项要有明显的剧情差异和后果
|
||||
5. 严格符合用户的改写意图,围绕用户指令展开剧情
|
||||
6. 保持原故事的人物性格、语言风格和世界观
|
||||
7. 对话要自然生动,描写要有画面感
|
||||
7. 如果有角色设定,必须保持角色性格和外貌的一致性
|
||||
8. 对话要自然生动,描写要有画面感
|
||||
|
||||
【关于结局 - 极其重要!】
|
||||
★★★ 每一条分支路径的尽头必须是结局节点 ★★★
|
||||
@@ -149,9 +183,10 @@ class AIService:
|
||||
- special 特殊结局:70-90分
|
||||
|
||||
【内容分段示例】
|
||||
"content": "他的声音在耳边响起,像是一阵温柔的风。\n\n\"我喜欢你。\"他说,目光坚定地看着你。\n\n你的心跳漏了一拍,一时间不知该如何回应。"
|
||||
|
||||
【输出格式】(严格JSON,不要有任何额外文字)
|
||||
"content": "他的声音在耳边响起,像是一阵温柔的风。\\n\\n\\"我喜欢你。\\"他说,目光坚定地看着你。\\n\\n你的心跳漏了一拍,一时间不知该如何回应。"
|
||||
"""
|
||||
|
||||
system_prompt_json = """【输出格式】(严格JSON,不要有任何额外文字)
|
||||
{
|
||||
"nodes": {
|
||||
"branch_1": {
|
||||
@@ -179,7 +214,7 @@ class AIService:
|
||||
]
|
||||
},
|
||||
"branch_ending_good": {
|
||||
"content": "好结局内容(200-400字)...\n\n【达成结局:xxx】",
|
||||
"content": "好结局内容(200-400字)...\\n\\n【达成结局:xxx】",
|
||||
"speaker": "旁白",
|
||||
"is_ending": true,
|
||||
"ending_name": "结局名称",
|
||||
@@ -187,7 +222,7 @@ class AIService:
|
||||
"ending_score": 90
|
||||
},
|
||||
"branch_ending_bad": {
|
||||
"content": "坏结局内容...\n\n【达成结局:xxx】",
|
||||
"content": "坏结局内容...\\n\\n【达成结局:xxx】",
|
||||
"speaker": "旁白",
|
||||
"is_ending": true,
|
||||
"ending_name": "结局名称",
|
||||
@@ -195,7 +230,7 @@ class AIService:
|
||||
"ending_score": 40
|
||||
},
|
||||
"branch_ending_neutral": {
|
||||
"content": "中立结局...\n\n【达成结局:xxx】",
|
||||
"content": "中立结局...\\n\\n【达成结局:xxx】",
|
||||
"speaker": "旁白",
|
||||
"is_ending": true,
|
||||
"ending_name": "结局名称",
|
||||
@@ -203,7 +238,7 @@ class AIService:
|
||||
"ending_score": 60
|
||||
},
|
||||
"branch_ending_special": {
|
||||
"content": "特殊结局...\n\n【达成结局:xxx】",
|
||||
"content": "特殊结局...\\n\\n【达成结局:xxx】",
|
||||
"speaker": "旁白",
|
||||
"is_ending": true,
|
||||
"ending_name": "结局名称",
|
||||
@@ -213,6 +248,8 @@ class AIService:
|
||||
},
|
||||
"entryNodeKey": "branch_1"
|
||||
}"""
|
||||
|
||||
system_prompt = system_prompt_header + system_prompt_json
|
||||
|
||||
# 构建用户提示词
|
||||
user_prompt_text = f"""【原故事信息】
|
||||
@@ -280,7 +317,8 @@ class AIService:
|
||||
story_category: str,
|
||||
ending_name: str,
|
||||
ending_content: str,
|
||||
user_prompt: str
|
||||
user_prompt: str,
|
||||
characters: List[Dict] = None
|
||||
) -> Optional[Dict[str, Any]]:
|
||||
"""
|
||||
AI续写结局,从结局开始续写新的剧情分支
|
||||
@@ -289,15 +327,19 @@ class AIService:
|
||||
print(f"[continue_ending] story_title={story_title}, category={story_category}")
|
||||
print(f"[continue_ending] ending_name={ending_name}")
|
||||
print(f"[continue_ending] user_prompt={user_prompt}")
|
||||
print(f"[continue_ending] characters数量={len(characters) if characters else 0}")
|
||||
print(f"[continue_ending] enabled={self.enabled}, api_key存在={bool(self.api_key)}")
|
||||
|
||||
if not self.enabled or not self.api_key:
|
||||
print(f"[continue_ending] 服务未启用或API Key为空,返回None")
|
||||
return None
|
||||
|
||||
# 构建系统提示词
|
||||
system_prompt = """你是一个专业的互动故事续写专家。用户已经到达故事的某个结局,现在想要从这个结局继续故事发展。
|
||||
# 格式化角色信息
|
||||
characters_text = format_characters_prompt(characters) if characters else ""
|
||||
|
||||
# 构建系统提示词
|
||||
system_prompt_header = f"""你是一个专业的互动故事续写专家。用户已经到达故事的某个结局,现在想要从这个结局继续故事发展。
|
||||
{characters_text}
|
||||
【任务】
|
||||
请从这个结局开始,创作故事的延续。新剧情必须紧接结局内容,仿佛故事并没有在此结束,而是有了新的发展。
|
||||
|
||||
@@ -308,7 +350,8 @@ class AIService:
|
||||
4. 每个非结局节点有 2 个选项,选项要有明显的剧情差异和后果
|
||||
5. 严格符合用户的续写意图,围绕用户指令展开剧情
|
||||
6. 保持原故事的人物性格、语言风格和世界观
|
||||
7. 对话要自然生动,描写要有画面感
|
||||
7. 如果有角色设定,必须保持角色性格和外貌的一致性
|
||||
8. 对话要自然生动,描写要有画面感
|
||||
|
||||
【关于新结局 - 极其重要!】
|
||||
★★★ 每一条分支路径的尽头必须是新结局节点 ★★★
|
||||
@@ -317,8 +360,9 @@ class AIService:
|
||||
- 结局名称 4-8 字,体现剧情走向
|
||||
- 如果有2个选项分支,最终必须有2个不同的结局
|
||||
- 每个结局必须有 "ending_score" 评分(0-100)
|
||||
"""
|
||||
|
||||
【输出格式】(严格JSON,不要有任何额外文字)
|
||||
system_prompt_json = """【输出格式】(严格JSON,不要有任何额外文字)
|
||||
{
|
||||
"nodes": {
|
||||
"continue_1": {
|
||||
@@ -357,6 +401,8 @@ class AIService:
|
||||
"entryNodeKey": "continue_1"
|
||||
}"""
|
||||
|
||||
system_prompt = system_prompt_header + system_prompt_json
|
||||
|
||||
# 构建用户提示词
|
||||
user_prompt_text = f"""【原故事信息】
|
||||
故事标题:{story_title}
|
||||
|
||||
Reference in New Issue
Block a user