feat: AI改写功能集成角色数据 + UI优化
- 新增story_characters表和seed_characters.sql种子数据(27个角色) - AI改写/续写功能注入角色信息(性别/年龄/外貌/性格) - 首页UI下移避让微信退出按钮 - 个人中心页面布局重构
This commit is contained in:
@@ -65,7 +65,7 @@ export default class HomeScene extends BaseScene {
|
|||||||
const stories = this.getFilteredStories();
|
const stories = this.getFilteredStories();
|
||||||
const cardHeight = 130;
|
const cardHeight = 130;
|
||||||
const gap = 12;
|
const gap = 12;
|
||||||
const startY = 175;
|
const startY = 190;
|
||||||
const tabHeight = 60;
|
const tabHeight = 60;
|
||||||
const contentBottom = startY + stories.length * (cardHeight + gap);
|
const contentBottom = startY + stories.length * (cardHeight + gap);
|
||||||
const visibleBottom = this.screenHeight - tabHeight;
|
const visibleBottom = this.screenHeight - tabHeight;
|
||||||
@@ -130,7 +130,7 @@ export default class HomeScene extends BaseScene {
|
|||||||
}
|
}
|
||||||
|
|
||||||
renderContentTabs(ctx) {
|
renderContentTabs(ctx) {
|
||||||
const tabY = 60;
|
const tabY = 75;
|
||||||
const tabWidth = (this.screenWidth - 30) / 4;
|
const tabWidth = (this.screenWidth - 30) / 4;
|
||||||
const padding = 15;
|
const padding = 15;
|
||||||
|
|
||||||
@@ -160,7 +160,7 @@ export default class HomeScene extends BaseScene {
|
|||||||
}
|
}
|
||||||
|
|
||||||
renderCategories(ctx) {
|
renderCategories(ctx) {
|
||||||
const startY = 95;
|
const startY = 110;
|
||||||
const tagHeight = 28;
|
const tagHeight = 28;
|
||||||
let x = 15 - this.categoryScrollX;
|
let x = 15 - this.categoryScrollX;
|
||||||
|
|
||||||
@@ -207,7 +207,7 @@ export default class HomeScene extends BaseScene {
|
|||||||
}
|
}
|
||||||
|
|
||||||
renderStoryList(ctx) {
|
renderStoryList(ctx) {
|
||||||
const startY = 140;
|
const startY = 155;
|
||||||
const cardHeight = 130;
|
const cardHeight = 130;
|
||||||
const cardMargin = 12;
|
const cardMargin = 12;
|
||||||
const stories = this.getFilteredStories();
|
const stories = this.getFilteredStories();
|
||||||
@@ -425,7 +425,7 @@ export default class HomeScene extends BaseScene {
|
|||||||
this.touchStartY = touch.clientY;
|
this.touchStartY = touch.clientY;
|
||||||
this.hasMoved = false;
|
this.hasMoved = false;
|
||||||
|
|
||||||
if (touch.clientY >= 90 && touch.clientY <= 130) {
|
if (touch.clientY >= 105 && touch.clientY <= 145) {
|
||||||
this.isCategoryDragging = true;
|
this.isCategoryDragging = true;
|
||||||
this.isDragging = false;
|
this.isDragging = false;
|
||||||
} else {
|
} else {
|
||||||
@@ -489,7 +489,7 @@ export default class HomeScene extends BaseScene {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 分类点击
|
// 分类点击
|
||||||
if (y >= 90 && y <= 130) {
|
if (y >= 105 && y <= 145) {
|
||||||
this.handleCategoryClick(x);
|
this.handleCategoryClick(x);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -530,7 +530,7 @@ export default class HomeScene extends BaseScene {
|
|||||||
}
|
}
|
||||||
|
|
||||||
handleStoryClick(x, y) {
|
handleStoryClick(x, y) {
|
||||||
const startY = 140;
|
const startY = 155;
|
||||||
const cardHeight = 130;
|
const cardHeight = 130;
|
||||||
const cardMargin = 12;
|
const cardMargin = 12;
|
||||||
const stories = this.getFilteredStories();
|
const stories = this.getFilteredStories();
|
||||||
|
|||||||
@@ -140,6 +140,7 @@ export default class ProfileScene extends BaseScene {
|
|||||||
this.renderUserCard(ctx);
|
this.renderUserCard(ctx);
|
||||||
this.renderTabs(ctx);
|
this.renderTabs(ctx);
|
||||||
this.renderList(ctx);
|
this.renderList(ctx);
|
||||||
|
this.renderLogoutButton(ctx);
|
||||||
}
|
}
|
||||||
|
|
||||||
renderBackground(ctx) {
|
renderBackground(ctx) {
|
||||||
@@ -160,12 +161,6 @@ export default class ProfileScene extends BaseScene {
|
|||||||
ctx.textAlign = 'center';
|
ctx.textAlign = 'center';
|
||||||
ctx.font = 'bold 17px sans-serif';
|
ctx.font = 'bold 17px sans-serif';
|
||||||
ctx.fillText('个人中心', this.screenWidth / 2, 35);
|
ctx.fillText('个人中心', this.screenWidth / 2, 35);
|
||||||
|
|
||||||
// 设置按钮
|
|
||||||
ctx.fillStyle = 'rgba(255,255,255,0.6)';
|
|
||||||
ctx.font = '18px sans-serif';
|
|
||||||
ctx.textAlign = 'right';
|
|
||||||
ctx.fillText('⚙', this.screenWidth - 20, 35);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
renderUserCard(ctx) {
|
renderUserCard(ctx) {
|
||||||
@@ -223,10 +218,24 @@ export default class ProfileScene extends BaseScene {
|
|||||||
ctx.textAlign = 'left';
|
ctx.textAlign = 'left';
|
||||||
ctx.fillStyle = '#ffffff';
|
ctx.fillStyle = '#ffffff';
|
||||||
ctx.font = 'bold 16px sans-serif';
|
ctx.font = 'bold 16px sans-serif';
|
||||||
ctx.fillText(user.nickname || '游客用户', avatarX + avatarSize + 15, avatarY + 22);
|
const nickname = user.nickname || '游客用户';
|
||||||
|
ctx.fillText(nickname, avatarX + avatarSize + 15, avatarY + 22);
|
||||||
|
|
||||||
|
// 昵称旁边的编辑按钮
|
||||||
|
const nicknameWidth = ctx.measureText(nickname).width;
|
||||||
|
const editNicknameBtnX = avatarX + avatarSize + 15 + nicknameWidth + 8;
|
||||||
|
ctx.fillStyle = 'rgba(255,255,255,0.15)';
|
||||||
|
this.roundRect(ctx, editNicknameBtnX, avatarY + 8, 24, 20, 10);
|
||||||
|
ctx.fill();
|
||||||
|
ctx.fillStyle = 'rgba(255,255,255,0.6)';
|
||||||
|
ctx.font = '11px sans-serif';
|
||||||
|
ctx.textAlign = 'center';
|
||||||
|
ctx.fillText('✎', editNicknameBtnX + 12, avatarY + 22);
|
||||||
|
this.editNicknameRect = { x: editNicknameBtnX, y: avatarY + 8, width: 24, height: 20 };
|
||||||
|
|
||||||
ctx.fillStyle = 'rgba(255,255,255,0.4)';
|
ctx.fillStyle = 'rgba(255,255,255,0.4)';
|
||||||
ctx.font = '11px sans-serif';
|
ctx.font = '11px sans-serif';
|
||||||
|
ctx.textAlign = 'left';
|
||||||
ctx.fillText(`ID: ${user.userId || '未登录'}`, avatarX + avatarSize + 15, avatarY + 42);
|
ctx.fillText(`ID: ${user.userId || '未登录'}`, avatarX + avatarSize + 15, avatarY + 42);
|
||||||
|
|
||||||
// 创作者标签
|
// 创作者标签
|
||||||
@@ -367,6 +376,34 @@ export default class ProfileScene extends BaseScene {
|
|||||||
ctx.restore();
|
ctx.restore();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 渲染底部退出登录按钮
|
||||||
|
renderLogoutButton(ctx) {
|
||||||
|
const btnW = 120;
|
||||||
|
const btnH = 40;
|
||||||
|
const btnX = (this.screenWidth - btnW) / 2;
|
||||||
|
const btnY = this.screenHeight - 70;
|
||||||
|
|
||||||
|
// 按钮背景
|
||||||
|
ctx.fillStyle = 'rgba(239, 68, 68, 0.15)';
|
||||||
|
this.roundRect(ctx, btnX, btnY, btnW, btnH, 20);
|
||||||
|
ctx.fill();
|
||||||
|
|
||||||
|
// 按钮边框
|
||||||
|
ctx.strokeStyle = 'rgba(239, 68, 68, 0.4)';
|
||||||
|
ctx.lineWidth = 1;
|
||||||
|
this.roundRect(ctx, btnX, btnY, btnW, btnH, 20);
|
||||||
|
ctx.stroke();
|
||||||
|
|
||||||
|
// 按钮文字
|
||||||
|
ctx.fillStyle = '#ef4444';
|
||||||
|
ctx.font = '14px sans-serif';
|
||||||
|
ctx.textAlign = 'center';
|
||||||
|
ctx.fillText('退出登录', this.screenWidth / 2, btnY + 26);
|
||||||
|
|
||||||
|
// 保存按钮区域
|
||||||
|
this.logoutBtnRect = { x: btnX, y: btnY, width: btnW, height: btnH };
|
||||||
|
}
|
||||||
|
|
||||||
// 渲染版本列表头部(返回按钮+故事标题)
|
// 渲染版本列表头部(返回按钮+故事标题)
|
||||||
renderVersionListHeader(ctx, startY) {
|
renderVersionListHeader(ctx, startY) {
|
||||||
const headerY = startY - 5;
|
const headerY = startY - 5;
|
||||||
@@ -764,17 +801,29 @@ export default class ProfileScene extends BaseScene {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 设置按钮(右上角)
|
// 退出登录按钮
|
||||||
if (y < 50 && x > this.screenWidth - 50) {
|
if (this.logoutBtnRect) {
|
||||||
this.showSettingsMenu();
|
const btn = this.logoutBtnRect;
|
||||||
|
if (x >= btn.x && x <= btn.x + btn.width && y >= btn.y && y <= btn.y + btn.height) {
|
||||||
|
this.confirmLogout();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 头像点击
|
// 头像点击(修改头像)
|
||||||
if (this.avatarRect) {
|
if (this.avatarRect) {
|
||||||
const rect = this.avatarRect;
|
const rect = this.avatarRect;
|
||||||
if (x >= rect.x && x <= rect.x + rect.width && y >= rect.y && y <= rect.y + rect.height) {
|
if (x >= rect.x && x <= rect.x + rect.width && y >= rect.y && y <= rect.y + rect.height) {
|
||||||
this.showAvatarOptions();
|
this.chooseAndUploadAvatar();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 昵称编辑按钮点击
|
||||||
|
if (this.editNicknameRect) {
|
||||||
|
const rect = this.editNicknameRect;
|
||||||
|
if (x >= rect.x && x <= rect.x + rect.width && y >= rect.y && y <= rect.y + rect.height) {
|
||||||
|
this.showEditNicknameDialog();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,6 +26,28 @@ class Story(Base):
|
|||||||
updated_at = Column(TIMESTAMP, server_default=func.now(), onupdate=func.now())
|
updated_at = Column(TIMESTAMP, server_default=func.now(), onupdate=func.now())
|
||||||
|
|
||||||
nodes = relationship("StoryNode", back_populates="story", cascade="all, delete-orphan")
|
nodes = relationship("StoryNode", back_populates="story", cascade="all, delete-orphan")
|
||||||
|
characters = relationship("StoryCharacter", back_populates="story", cascade="all, delete-orphan")
|
||||||
|
|
||||||
|
|
||||||
|
class StoryCharacter(Base):
|
||||||
|
"""故事角色表"""
|
||||||
|
__tablename__ = "story_characters"
|
||||||
|
|
||||||
|
id = Column(Integer, primary_key=True, autoincrement=True)
|
||||||
|
story_id = Column(Integer, ForeignKey("stories.id", ondelete="CASCADE"), nullable=False)
|
||||||
|
name = Column(String(50), nullable=False)
|
||||||
|
role_type = Column(String(20), default="supporting") # protagonist/antagonist/supporting
|
||||||
|
gender = Column(String(10), default="")
|
||||||
|
age_range = Column(String(20), default="")
|
||||||
|
appearance = Column(Text) # 外貌描述
|
||||||
|
personality = Column(Text) # 性格描述
|
||||||
|
background = Column(Text) # 背景故事
|
||||||
|
avatar_prompt = Column(Text) # AI绘图提示词
|
||||||
|
avatar_url = Column(String(500), default="")
|
||||||
|
created_at = Column(TIMESTAMP, server_default=func.now())
|
||||||
|
updated_at = Column(TIMESTAMP, server_default=func.now(), onupdate=func.now())
|
||||||
|
|
||||||
|
story = relationship("Story", back_populates="characters")
|
||||||
|
|
||||||
|
|
||||||
class StoryNode(Base):
|
class StoryNode(Base):
|
||||||
|
|||||||
@@ -10,11 +10,32 @@ from typing import List, Optional
|
|||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
from app.database import get_db
|
from app.database import get_db
|
||||||
from app.models.story import Story, StoryDraft, DraftStatus
|
from app.models.story import Story, StoryDraft, DraftStatus, StoryCharacter
|
||||||
|
|
||||||
router = APIRouter(prefix="/drafts", tags=["草稿箱"])
|
router = APIRouter(prefix="/drafts", tags=["草稿箱"])
|
||||||
|
|
||||||
|
|
||||||
|
# ============ 辅助函数 ============
|
||||||
|
|
||||||
|
async def get_story_characters(db: AsyncSession, story_id: int) -> List[dict]:
|
||||||
|
"""获取故事的所有角色并转为字典列表"""
|
||||||
|
result = await db.execute(
|
||||||
|
select(StoryCharacter).where(StoryCharacter.story_id == story_id)
|
||||||
|
)
|
||||||
|
characters = result.scalars().all()
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
"name": c.name,
|
||||||
|
"role_type": c.role_type,
|
||||||
|
"gender": c.gender,
|
||||||
|
"age_range": c.age_range,
|
||||||
|
"appearance": c.appearance,
|
||||||
|
"personality": c.personality
|
||||||
|
}
|
||||||
|
for c in characters
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
# ============ 请求/响应模型 ============
|
# ============ 请求/响应模型 ============
|
||||||
|
|
||||||
class PathHistoryItem(BaseModel):
|
class PathHistoryItem(BaseModel):
|
||||||
@@ -98,6 +119,9 @@ async def process_ai_rewrite(draft_id: int):
|
|||||||
await db.commit()
|
await db.commit()
|
||||||
return
|
return
|
||||||
|
|
||||||
|
# 获取故事角色
|
||||||
|
characters = await get_story_characters(db, story.id)
|
||||||
|
|
||||||
# 转换路径历史格式
|
# 转换路径历史格式
|
||||||
path_history = draft.path_history or []
|
path_history = draft.path_history or []
|
||||||
|
|
||||||
@@ -107,7 +131,8 @@ async def process_ai_rewrite(draft_id: int):
|
|||||||
story_category=story.category or "未知",
|
story_category=story.category or "未知",
|
||||||
path_history=path_history,
|
path_history=path_history,
|
||||||
current_content=draft.current_content or "",
|
current_content=draft.current_content or "",
|
||||||
user_prompt=draft.user_prompt
|
user_prompt=draft.user_prompt,
|
||||||
|
characters=characters
|
||||||
)
|
)
|
||||||
|
|
||||||
if ai_result and ai_result.get("nodes"):
|
if ai_result and ai_result.get("nodes"):
|
||||||
@@ -171,6 +196,9 @@ async def process_ai_rewrite_ending(draft_id: int):
|
|||||||
await db.commit()
|
await db.commit()
|
||||||
return
|
return
|
||||||
|
|
||||||
|
# 获取故事角色
|
||||||
|
characters = await get_story_characters(db, story.id)
|
||||||
|
|
||||||
# 从草稿字段获取结局信息
|
# 从草稿字段获取结局信息
|
||||||
ending_name = draft.current_node_key or "未知结局"
|
ending_name = draft.current_node_key or "未知结局"
|
||||||
ending_content = draft.current_content or ""
|
ending_content = draft.current_content or ""
|
||||||
@@ -181,7 +209,8 @@ async def process_ai_rewrite_ending(draft_id: int):
|
|||||||
story_category=story.category or "未知",
|
story_category=story.category or "未知",
|
||||||
ending_name=ending_name,
|
ending_name=ending_name,
|
||||||
ending_content=ending_content,
|
ending_content=ending_content,
|
||||||
user_prompt=draft.user_prompt
|
user_prompt=draft.user_prompt,
|
||||||
|
characters=characters
|
||||||
)
|
)
|
||||||
|
|
||||||
if ai_result and ai_result.get("content"):
|
if ai_result and ai_result.get("content"):
|
||||||
@@ -266,6 +295,9 @@ async def process_ai_continue_ending(draft_id: int):
|
|||||||
await db.commit()
|
await db.commit()
|
||||||
return
|
return
|
||||||
|
|
||||||
|
# 获取故事角色
|
||||||
|
characters = await get_story_characters(db, story.id)
|
||||||
|
|
||||||
# 从草稿字段获取结局信息
|
# 从草稿字段获取结局信息
|
||||||
ending_name = draft.current_node_key or "未知结局"
|
ending_name = draft.current_node_key or "未知结局"
|
||||||
ending_content = draft.current_content or ""
|
ending_content = draft.current_content or ""
|
||||||
@@ -276,7 +308,8 @@ async def process_ai_continue_ending(draft_id: int):
|
|||||||
story_category=story.category or "未知",
|
story_category=story.category or "未知",
|
||||||
ending_name=ending_name,
|
ending_name=ending_name,
|
||||||
ending_content=ending_content,
|
ending_content=ending_content,
|
||||||
user_prompt=draft.user_prompt
|
user_prompt=draft.user_prompt,
|
||||||
|
characters=characters
|
||||||
)
|
)
|
||||||
|
|
||||||
if ai_result and ai_result.get("nodes"):
|
if ai_result and ai_result.get("nodes"):
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ from typing import Optional, List
|
|||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
|
|
||||||
from app.database import get_db
|
from app.database import get_db
|
||||||
from app.models.story import Story, StoryNode, StoryChoice
|
from app.models.story import Story, StoryNode, StoryChoice, StoryCharacter
|
||||||
|
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
|
|
||||||
@@ -214,6 +214,22 @@ async def ai_rewrite_ending(story_id: int, request: RewriteRequest, db: AsyncSes
|
|||||||
if not story:
|
if not story:
|
||||||
raise HTTPException(status_code=404, detail="故事不存在")
|
raise HTTPException(status_code=404, detail="故事不存在")
|
||||||
|
|
||||||
|
# 获取故事角色
|
||||||
|
char_result = await db.execute(
|
||||||
|
select(StoryCharacter).where(StoryCharacter.story_id == story_id)
|
||||||
|
)
|
||||||
|
characters = [
|
||||||
|
{
|
||||||
|
"name": c.name,
|
||||||
|
"role_type": c.role_type,
|
||||||
|
"gender": c.gender,
|
||||||
|
"age_range": c.age_range,
|
||||||
|
"appearance": c.appearance,
|
||||||
|
"personality": c.personality
|
||||||
|
}
|
||||||
|
for c in char_result.scalars().all()
|
||||||
|
]
|
||||||
|
|
||||||
# 调用 AI 服务
|
# 调用 AI 服务
|
||||||
from app.services.ai import ai_service
|
from app.services.ai import ai_service
|
||||||
|
|
||||||
@@ -222,7 +238,8 @@ async def ai_rewrite_ending(story_id: int, request: RewriteRequest, db: AsyncSes
|
|||||||
story_category=story.category or "未知",
|
story_category=story.category or "未知",
|
||||||
ending_name=request.ending_name or "未知结局",
|
ending_name=request.ending_name or "未知结局",
|
||||||
ending_content=request.ending_content or "",
|
ending_content=request.ending_content or "",
|
||||||
user_prompt=request.prompt
|
user_prompt=request.prompt,
|
||||||
|
characters=characters
|
||||||
)
|
)
|
||||||
|
|
||||||
if ai_result and ai_result.get("content"):
|
if ai_result and ai_result.get("content"):
|
||||||
@@ -301,6 +318,22 @@ async def ai_rewrite_branch(
|
|||||||
if not story:
|
if not story:
|
||||||
raise HTTPException(status_code=404, detail="故事不存在")
|
raise HTTPException(status_code=404, detail="故事不存在")
|
||||||
|
|
||||||
|
# 获取故事角色
|
||||||
|
char_result = await db.execute(
|
||||||
|
select(StoryCharacter).where(StoryCharacter.story_id == story_id)
|
||||||
|
)
|
||||||
|
characters = [
|
||||||
|
{
|
||||||
|
"name": c.name,
|
||||||
|
"role_type": c.role_type,
|
||||||
|
"gender": c.gender,
|
||||||
|
"age_range": c.age_range,
|
||||||
|
"appearance": c.appearance,
|
||||||
|
"personality": c.personality
|
||||||
|
}
|
||||||
|
for c in char_result.scalars().all()
|
||||||
|
]
|
||||||
|
|
||||||
# 将 Pydantic 模型转换为字典列表
|
# 将 Pydantic 模型转换为字典列表
|
||||||
path_history = [
|
path_history = [
|
||||||
{"nodeKey": item.nodeKey, "content": item.content, "choice": item.choice}
|
{"nodeKey": item.nodeKey, "content": item.content, "choice": item.choice}
|
||||||
@@ -315,7 +348,8 @@ async def ai_rewrite_branch(
|
|||||||
story_category=story.category or "未知",
|
story_category=story.category or "未知",
|
||||||
path_history=path_history,
|
path_history=path_history,
|
||||||
current_content=request.currentContent,
|
current_content=request.currentContent,
|
||||||
user_prompt=request.prompt
|
user_prompt=request.prompt,
|
||||||
|
characters=characters
|
||||||
)
|
)
|
||||||
|
|
||||||
if ai_result and ai_result.get("nodes"):
|
if ai_result and ai_result.get("nodes"):
|
||||||
|
|||||||
@@ -7,6 +7,28 @@ import httpx
|
|||||||
import json
|
import json
|
||||||
import re
|
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:
|
class AIService:
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
from app.config import get_settings
|
from app.config import get_settings
|
||||||
@@ -49,7 +71,8 @@ class AIService:
|
|||||||
story_category: str,
|
story_category: str,
|
||||||
ending_name: str,
|
ending_name: str,
|
||||||
ending_content: str,
|
ending_content: str,
|
||||||
user_prompt: str
|
user_prompt: str,
|
||||||
|
characters: List[Dict] = None
|
||||||
) -> Optional[Dict[str, Any]]:
|
) -> Optional[Dict[str, Any]]:
|
||||||
"""
|
"""
|
||||||
AI改写结局
|
AI改写结局
|
||||||
@@ -58,14 +81,19 @@ class AIService:
|
|||||||
if not self.enabled or not self.api_key:
|
if not self.enabled or not self.api_key:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
# 格式化角色信息
|
||||||
|
characters_text = format_characters_prompt(characters) if characters else ""
|
||||||
|
|
||||||
# 构建Prompt
|
# 构建Prompt
|
||||||
system_prompt = """你是一个专业的互动故事创作专家。根据用户的改写指令,重新创作故事结局。
|
system_prompt = f"""你是一个专业的互动故事创作专家。根据用户的改写指令,重新创作故事结局。
|
||||||
|
{characters_text}
|
||||||
要求:
|
要求:
|
||||||
1. 保持原故事的世界观和人物性格
|
1. 保持原故事的世界观和人物性格
|
||||||
2. 结局要有张力和情感冲击
|
2. 结局要有张力和情感冲击
|
||||||
3. 结局内容字数控制在200-400字
|
3. 结局内容字数控制在200-400字
|
||||||
4. 为新结局取一个4-8字的新名字,体现改写后的剧情走向
|
4. 为新结局取一个4-8字的新名字,体现改写后的剧情走向
|
||||||
5. 输出格式必须是JSON:{"ending_name": "新结局名称", "content": "结局内容"}"""
|
5. 如果有角色设定,必须保持角色性格和外貌描述的一致性
|
||||||
|
6. 输出格式必须是JSON:{{"ending_name": "新结局名称", "content": "结局内容"}}"""
|
||||||
|
|
||||||
user_prompt_text = f"""故事标题:{story_title}
|
user_prompt_text = f"""故事标题:{story_title}
|
||||||
故事分类:{story_category}
|
故事分类:{story_category}
|
||||||
@@ -97,7 +125,8 @@ class AIService:
|
|||||||
story_category: str,
|
story_category: str,
|
||||||
path_history: List[Dict[str, str]],
|
path_history: List[Dict[str, str]],
|
||||||
current_content: str,
|
current_content: str,
|
||||||
user_prompt: str
|
user_prompt: str,
|
||||||
|
characters: List[Dict] = None
|
||||||
) -> Optional[Dict[str, Any]]:
|
) -> Optional[Dict[str, Any]]:
|
||||||
"""
|
"""
|
||||||
AI改写中间章节,生成新的剧情分支
|
AI改写中间章节,生成新的剧情分支
|
||||||
@@ -107,12 +136,16 @@ class AIService:
|
|||||||
print(f"[rewrite_branch] user_prompt={user_prompt}")
|
print(f"[rewrite_branch] user_prompt={user_prompt}")
|
||||||
print(f"[rewrite_branch] path_history长度={len(path_history)}")
|
print(f"[rewrite_branch] path_history长度={len(path_history)}")
|
||||||
print(f"[rewrite_branch] current_content长度={len(current_content)}")
|
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)}")
|
print(f"[rewrite_branch] enabled={self.enabled}, api_key存在={bool(self.api_key)}")
|
||||||
|
|
||||||
if not self.enabled or not self.api_key:
|
if not self.enabled or not self.api_key:
|
||||||
print(f"[rewrite_branch] 服务未启用或API Key为空,返回None")
|
print(f"[rewrite_branch] 服务未启用或API Key为空,返回None")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
# 格式化角色信息
|
||||||
|
characters_text = format_characters_prompt(characters) if characters else ""
|
||||||
|
|
||||||
# 构建路径历史文本
|
# 构建路径历史文本
|
||||||
path_text = ""
|
path_text = ""
|
||||||
for i, item in enumerate(path_history, 1):
|
for i, item in enumerate(path_history, 1):
|
||||||
@@ -121,19 +154,20 @@ class AIService:
|
|||||||
path_text += f" → 用户选择:{item['choice']}\n"
|
path_text += f" → 用户选择:{item['choice']}\n"
|
||||||
|
|
||||||
# 构建系统提示词
|
# 构建系统提示词
|
||||||
system_prompt = """你是一个专业的互动故事续写专家。用户正在玩一个互动故事,想要在当前位置改变剧情走向。
|
system_prompt_header = f"""你是一个专业的互动故事续写专家。用户正在玩一个互动故事,想要在当前位置改变剧情走向。
|
||||||
|
{characters_text}
|
||||||
【任务】
|
【任务】
|
||||||
请从当前节点开始,创作新的剧情分支。新剧情的第一句话必须紧接当前节点最后的情节,仿佛是同一段故事的自然延续,不能有任何跳跃感。
|
请从当前节点开始,创作新的剧情分支。新剧情的第一句话必须紧接当前节点最后的情节,仿佛是同一段故事的自然延续,不能有任何跳跃感。
|
||||||
|
|
||||||
【写作要求】
|
【写作要求】
|
||||||
1. 第一个节点必须紧密衔接当前剧情,像是同一段话的下一句
|
1. 第一个节点必须紧密衔接当前剧情,像是同一段话的下一句
|
||||||
2. 生成 4-6 个新节点,形成有层次的剧情发展(起承转合)
|
2. 生成 4-6 个新节点,形成有层次的剧情发展(起承转合)
|
||||||
3. 每个节点内容 150-300 字,要分 2-3 个自然段(用\n\n分隔),包含:场景描写、人物对话、心理活动
|
3. 每个节点内容 150-300 字,要分 2-3 个自然段(用\\n\\n分隔),包含:场景描写、人物对话、心理活动
|
||||||
4. 每个非结局节点有 2 个选项,选项要有明显的剧情差异和后果
|
4. 每个非结局节点有 2 个选项,选项要有明显的剧情差异和后果
|
||||||
5. 严格符合用户的改写意图,围绕用户指令展开剧情
|
5. 严格符合用户的改写意图,围绕用户指令展开剧情
|
||||||
6. 保持原故事的人物性格、语言风格和世界观
|
6. 保持原故事的人物性格、语言风格和世界观
|
||||||
7. 对话要自然生动,描写要有画面感
|
7. 如果有角色设定,必须保持角色性格和外貌的一致性
|
||||||
|
8. 对话要自然生动,描写要有画面感
|
||||||
|
|
||||||
【关于结局 - 极其重要!】
|
【关于结局 - 极其重要!】
|
||||||
★★★ 每一条分支路径的尽头必须是结局节点 ★★★
|
★★★ 每一条分支路径的尽头必须是结局节点 ★★★
|
||||||
@@ -149,9 +183,10 @@ class AIService:
|
|||||||
- special 特殊结局:70-90分
|
- special 特殊结局:70-90分
|
||||||
|
|
||||||
【内容分段示例】
|
【内容分段示例】
|
||||||
"content": "他的声音在耳边响起,像是一阵温柔的风。\n\n\"我喜欢你。\"他说,目光坚定地看着你。\n\n你的心跳漏了一拍,一时间不知该如何回应。"
|
"content": "他的声音在耳边响起,像是一阵温柔的风。\\n\\n\\"我喜欢你。\\"他说,目光坚定地看着你。\\n\\n你的心跳漏了一拍,一时间不知该如何回应。"
|
||||||
|
"""
|
||||||
|
|
||||||
【输出格式】(严格JSON,不要有任何额外文字)
|
system_prompt_json = """【输出格式】(严格JSON,不要有任何额外文字)
|
||||||
{
|
{
|
||||||
"nodes": {
|
"nodes": {
|
||||||
"branch_1": {
|
"branch_1": {
|
||||||
@@ -179,7 +214,7 @@ class AIService:
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"branch_ending_good": {
|
"branch_ending_good": {
|
||||||
"content": "好结局内容(200-400字)...\n\n【达成结局:xxx】",
|
"content": "好结局内容(200-400字)...\\n\\n【达成结局:xxx】",
|
||||||
"speaker": "旁白",
|
"speaker": "旁白",
|
||||||
"is_ending": true,
|
"is_ending": true,
|
||||||
"ending_name": "结局名称",
|
"ending_name": "结局名称",
|
||||||
@@ -187,7 +222,7 @@ class AIService:
|
|||||||
"ending_score": 90
|
"ending_score": 90
|
||||||
},
|
},
|
||||||
"branch_ending_bad": {
|
"branch_ending_bad": {
|
||||||
"content": "坏结局内容...\n\n【达成结局:xxx】",
|
"content": "坏结局内容...\\n\\n【达成结局:xxx】",
|
||||||
"speaker": "旁白",
|
"speaker": "旁白",
|
||||||
"is_ending": true,
|
"is_ending": true,
|
||||||
"ending_name": "结局名称",
|
"ending_name": "结局名称",
|
||||||
@@ -195,7 +230,7 @@ class AIService:
|
|||||||
"ending_score": 40
|
"ending_score": 40
|
||||||
},
|
},
|
||||||
"branch_ending_neutral": {
|
"branch_ending_neutral": {
|
||||||
"content": "中立结局...\n\n【达成结局:xxx】",
|
"content": "中立结局...\\n\\n【达成结局:xxx】",
|
||||||
"speaker": "旁白",
|
"speaker": "旁白",
|
||||||
"is_ending": true,
|
"is_ending": true,
|
||||||
"ending_name": "结局名称",
|
"ending_name": "结局名称",
|
||||||
@@ -203,7 +238,7 @@ class AIService:
|
|||||||
"ending_score": 60
|
"ending_score": 60
|
||||||
},
|
},
|
||||||
"branch_ending_special": {
|
"branch_ending_special": {
|
||||||
"content": "特殊结局...\n\n【达成结局:xxx】",
|
"content": "特殊结局...\\n\\n【达成结局:xxx】",
|
||||||
"speaker": "旁白",
|
"speaker": "旁白",
|
||||||
"is_ending": true,
|
"is_ending": true,
|
||||||
"ending_name": "结局名称",
|
"ending_name": "结局名称",
|
||||||
@@ -214,6 +249,8 @@ class AIService:
|
|||||||
"entryNodeKey": "branch_1"
|
"entryNodeKey": "branch_1"
|
||||||
}"""
|
}"""
|
||||||
|
|
||||||
|
system_prompt = system_prompt_header + system_prompt_json
|
||||||
|
|
||||||
# 构建用户提示词
|
# 构建用户提示词
|
||||||
user_prompt_text = f"""【原故事信息】
|
user_prompt_text = f"""【原故事信息】
|
||||||
故事标题:{story_title}
|
故事标题:{story_title}
|
||||||
@@ -280,7 +317,8 @@ class AIService:
|
|||||||
story_category: str,
|
story_category: str,
|
||||||
ending_name: str,
|
ending_name: str,
|
||||||
ending_content: str,
|
ending_content: str,
|
||||||
user_prompt: str
|
user_prompt: str,
|
||||||
|
characters: List[Dict] = None
|
||||||
) -> Optional[Dict[str, Any]]:
|
) -> Optional[Dict[str, Any]]:
|
||||||
"""
|
"""
|
||||||
AI续写结局,从结局开始续写新的剧情分支
|
AI续写结局,从结局开始续写新的剧情分支
|
||||||
@@ -289,15 +327,19 @@ class AIService:
|
|||||||
print(f"[continue_ending] story_title={story_title}, category={story_category}")
|
print(f"[continue_ending] story_title={story_title}, category={story_category}")
|
||||||
print(f"[continue_ending] ending_name={ending_name}")
|
print(f"[continue_ending] ending_name={ending_name}")
|
||||||
print(f"[continue_ending] user_prompt={user_prompt}")
|
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)}")
|
print(f"[continue_ending] enabled={self.enabled}, api_key存在={bool(self.api_key)}")
|
||||||
|
|
||||||
if not self.enabled or not self.api_key:
|
if not self.enabled or not self.api_key:
|
||||||
print(f"[continue_ending] 服务未启用或API Key为空,返回None")
|
print(f"[continue_ending] 服务未启用或API Key为空,返回None")
|
||||||
return 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 个选项,选项要有明显的剧情差异和后果
|
4. 每个非结局节点有 2 个选项,选项要有明显的剧情差异和后果
|
||||||
5. 严格符合用户的续写意图,围绕用户指令展开剧情
|
5. 严格符合用户的续写意图,围绕用户指令展开剧情
|
||||||
6. 保持原故事的人物性格、语言风格和世界观
|
6. 保持原故事的人物性格、语言风格和世界观
|
||||||
7. 对话要自然生动,描写要有画面感
|
7. 如果有角色设定,必须保持角色性格和外貌的一致性
|
||||||
|
8. 对话要自然生动,描写要有画面感
|
||||||
|
|
||||||
【关于新结局 - 极其重要!】
|
【关于新结局 - 极其重要!】
|
||||||
★★★ 每一条分支路径的尽头必须是新结局节点 ★★★
|
★★★ 每一条分支路径的尽头必须是新结局节点 ★★★
|
||||||
@@ -317,8 +360,9 @@ class AIService:
|
|||||||
- 结局名称 4-8 字,体现剧情走向
|
- 结局名称 4-8 字,体现剧情走向
|
||||||
- 如果有2个选项分支,最终必须有2个不同的结局
|
- 如果有2个选项分支,最终必须有2个不同的结局
|
||||||
- 每个结局必须有 "ending_score" 评分(0-100)
|
- 每个结局必须有 "ending_score" 评分(0-100)
|
||||||
|
"""
|
||||||
|
|
||||||
【输出格式】(严格JSON,不要有任何额外文字)
|
system_prompt_json = """【输出格式】(严格JSON,不要有任何额外文字)
|
||||||
{
|
{
|
||||||
"nodes": {
|
"nodes": {
|
||||||
"continue_1": {
|
"continue_1": {
|
||||||
@@ -357,6 +401,8 @@ class AIService:
|
|||||||
"entryNodeKey": "continue_1"
|
"entryNodeKey": "continue_1"
|
||||||
}"""
|
}"""
|
||||||
|
|
||||||
|
system_prompt = system_prompt_header + system_prompt_json
|
||||||
|
|
||||||
# 构建用户提示词
|
# 构建用户提示词
|
||||||
user_prompt_text = f"""【原故事信息】
|
user_prompt_text = f"""【原故事信息】
|
||||||
故事标题:{story_title}
|
故事标题:{story_title}
|
||||||
|
|||||||
@@ -176,3 +176,41 @@ CREATE TABLE IF NOT EXISTS `play_records` (
|
|||||||
CONSTRAINT `play_records_ibfk_1` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE,
|
CONSTRAINT `play_records_ibfk_1` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE,
|
||||||
CONSTRAINT `play_records_ibfk_2` FOREIGN KEY (`story_id`) REFERENCES `stories` (`id`) ON DELETE CASCADE
|
CONSTRAINT `play_records_ibfk_2` FOREIGN KEY (`story_id`) REFERENCES `stories` (`id`) ON DELETE CASCADE
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='游玩记录表';
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='游玩记录表';
|
||||||
|
|
||||||
|
-- ============================================
|
||||||
|
-- 9. 故事角色表
|
||||||
|
-- ============================================
|
||||||
|
CREATE TABLE IF NOT EXISTS `story_characters` (
|
||||||
|
`id` INT NOT NULL AUTO_INCREMENT,
|
||||||
|
`story_id` INT NOT NULL COMMENT '所属故事ID',
|
||||||
|
`name` VARCHAR(50) NOT NULL COMMENT '角色名称',
|
||||||
|
`role_type` VARCHAR(20) DEFAULT 'supporting' COMMENT '角色类型: protagonist/antagonist/supporting',
|
||||||
|
`gender` VARCHAR(10) DEFAULT '' COMMENT '性别: male/female/unknown',
|
||||||
|
`age_range` VARCHAR(20) DEFAULT '' COMMENT '年龄段: child/teen/young/middle/old',
|
||||||
|
`appearance` TEXT COMMENT '外貌描述(用于生成图片)',
|
||||||
|
`personality` TEXT COMMENT '性格描述',
|
||||||
|
`background` TEXT COMMENT '背景故事',
|
||||||
|
`avatar_prompt` TEXT COMMENT 'AI绘图提示词',
|
||||||
|
`avatar_url` VARCHAR(500) DEFAULT '' COMMENT '角色头像URL',
|
||||||
|
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
`updated_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||||
|
PRIMARY KEY (`id`),
|
||||||
|
KEY `idx_story_id` (`story_id`),
|
||||||
|
CONSTRAINT `story_characters_ibfk_1` FOREIGN KEY (`story_id`) REFERENCES `stories` (`id`) ON DELETE CASCADE
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='故事角色表';
|
||||||
|
|
||||||
|
-- ============================================
|
||||||
|
-- 10. 节点角色关联表
|
||||||
|
-- ============================================
|
||||||
|
CREATE TABLE IF NOT EXISTS `node_characters` (
|
||||||
|
`id` INT NOT NULL AUTO_INCREMENT,
|
||||||
|
`story_id` INT NOT NULL COMMENT '故事ID',
|
||||||
|
`node_key` VARCHAR(50) NOT NULL COMMENT '节点key',
|
||||||
|
`character_id` INT NOT NULL COMMENT '角色ID',
|
||||||
|
`is_speaker` TINYINT(1) DEFAULT 0 COMMENT '是否为该节点说话者',
|
||||||
|
`emotion` VARCHAR(30) DEFAULT 'neutral' COMMENT '该节点情绪: happy/sad/angry/neutral等',
|
||||||
|
PRIMARY KEY (`id`),
|
||||||
|
KEY `idx_story_node` (`story_id`, `node_key`),
|
||||||
|
KEY `idx_character` (`character_id`),
|
||||||
|
CONSTRAINT `node_characters_ibfk_1` FOREIGN KEY (`character_id`) REFERENCES `story_characters` (`id`) ON DELETE CASCADE
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='节点角色关联表';
|
||||||
|
|||||||
209
server/sql/seed_characters.sql
Normal file
209
server/sql/seed_characters.sql
Normal file
@@ -0,0 +1,209 @@
|
|||||||
|
-- 角色种子数据
|
||||||
|
USE stardom_story;
|
||||||
|
|
||||||
|
-- ============================================
|
||||||
|
-- 故事1: 总裁的替身新娘
|
||||||
|
-- ============================================
|
||||||
|
INSERT INTO story_characters (id, story_id, name, role_type, gender, age_range, appearance, personality, avatar_prompt) VALUES
|
||||||
|
(1, 1, '林诗语', 'protagonist', 'female', 'young', '清秀温婉的面容,一双明亮的杏眼,乌黑长发及腰,身材纤细,气质如兰', '善良隐忍,聪慧坚韧,温柔但有底线', 'young chinese woman, gentle beauty, long black hair, bright almond eyes, slender figure, elegant temperament, wedding dress'),
|
||||||
|
(2, 1, '陆景深', 'protagonist', 'male', 'young', '剑眉星目,薄唇紧抿,高大挺拔,气势凌厉,眼神冰冷如寒潭', '外冷内热,霸道专一,深情而克制', 'handsome chinese man, sharp eyebrows, thin lips, tall and imposing, cold piercing eyes, black suit, CEO style'),
|
||||||
|
(3, 1, '林诗韵', 'supporting', 'female', 'young', '容貌出众,浓妆艳抹,身姿妖娆', '任性自私,贪慕虚荣', 'beautiful chinese woman, heavy makeup, glamorous figure, designer clothes');
|
||||||
|
|
||||||
|
INSERT INTO node_characters (story_id, node_key, character_id, is_speaker, emotion) VALUES
|
||||||
|
(1, 'start', 1, 0, 'nervous'),
|
||||||
|
(1, 'start', 2, 0, 'neutral'),
|
||||||
|
(1, 'choice1_a', 1, 0, 'anxious'),
|
||||||
|
(1, 'choice1_a', 2, 1, 'cold'),
|
||||||
|
(1, 'choice1_b', 1, 0, 'scared'),
|
||||||
|
(1, 'choice1_b', 2, 1, 'amused'),
|
||||||
|
(1, 'choice2_a', 1, 1, 'honest'),
|
||||||
|
(1, 'choice2_a', 2, 0, 'interested'),
|
||||||
|
(1, 'choice2_b', 1, 1, 'nervous'),
|
||||||
|
(1, 'choice2_b', 2, 0, 'smirking'),
|
||||||
|
(1, 'ending_good', 1, 0, 'happy'),
|
||||||
|
(1, 'ending_good', 2, 1, 'loving'),
|
||||||
|
(1, 'ending_normal', 1, 1, 'calm'),
|
||||||
|
(1, 'ending_normal', 2, 0, 'sad'),
|
||||||
|
(1, 'ending_bad', 1, 0, 'heartbroken'),
|
||||||
|
(1, 'ending_bad', 2, 1, 'cruel');
|
||||||
|
|
||||||
|
-- ============================================
|
||||||
|
-- 故事2: 密室中的第四个人
|
||||||
|
-- ============================================
|
||||||
|
INSERT INTO story_characters (id, story_id, name, role_type, gender, age_range, appearance, personality, avatar_prompt) VALUES
|
||||||
|
(4, 2, '王教授', 'supporting', 'male', 'middle', '戴眼镜的中年男人,头发花白,穿着整洁的西装', '学者气质,表面儒雅,实际隐藏着秘密', 'middle-aged chinese man, glasses, graying hair, neat suit, scholarly appearance'),
|
||||||
|
(5, 2, '苏小姐', 'supporting', 'female', 'young', '穿着名牌,妆容精致,身上有淡淡香水味', '看似高傲,内心脆弱', 'young elegant chinese woman, designer clothes, delicate makeup, wealthy appearance'),
|
||||||
|
(6, 2, '李明', 'antagonist', 'male', 'young', '沉默寡言的青年,眼神阴沉,嘴角常带冷笑', '隐忍复仇,城府极深', 'young chinese man, gloomy eyes, cold expression, dark casual clothes');
|
||||||
|
|
||||||
|
INSERT INTO node_characters (story_id, node_key, character_id, is_speaker, emotion) VALUES
|
||||||
|
(2, 'start', 4, 0, 'dead'),
|
||||||
|
(2, 'start', 5, 0, 'shocked'),
|
||||||
|
(2, 'start', 6, 0, 'calm'),
|
||||||
|
(2, 'investigate_room', 4, 0, 'dead'),
|
||||||
|
(2, 'question_su', 5, 1, 'nervous'),
|
||||||
|
(2, 'question_li', 6, 1, 'cold'),
|
||||||
|
(2, 'accuse_su', 5, 1, 'crying'),
|
||||||
|
(2, 'accuse_li', 6, 1, 'vengeful'),
|
||||||
|
(2, 'ending_truth', 6, 1, 'resigned'),
|
||||||
|
(2, 'ending_wrong', 5, 0, 'wronged');
|
||||||
|
|
||||||
|
-- ============================================
|
||||||
|
-- 故事3: 凤临天下
|
||||||
|
-- ============================================
|
||||||
|
INSERT INTO story_characters (id, story_id, name, role_type, gender, age_range, appearance, personality, avatar_prompt) VALUES
|
||||||
|
(7, 3, '林清婉', 'protagonist', 'female', 'young', '眉目如画,肤若凝脂,身姿窈窕,举止端庄', '外柔内刚,隐忍聪慧,心思缜密', 'beautiful ancient chinese woman, delicate features, fair skin, graceful posture, palace costume, elegant hairpin'),
|
||||||
|
(8, 3, '皇帝', 'protagonist', 'male', 'young', '龙袍加身,眉宇威严,俊朗不凡,气势天成', '深沉多疑,霸道深情', 'handsome ancient chinese emperor, dragon robe, majestic presence, piercing eyes, imperial crown'),
|
||||||
|
(9, 3, '皇后', 'antagonist', 'female', 'middle', '珠翠环绕,气度雍容,凤冠霞帔,笑容不达眼底', '城府极深,手段狠辣', 'ancient chinese empress, phoenix crown, luxurious robes, dignified bearing, calculating smile');
|
||||||
|
|
||||||
|
INSERT INTO node_characters (story_id, node_key, character_id, is_speaker, emotion) VALUES
|
||||||
|
(3, 'start', 7, 0, 'nervous'),
|
||||||
|
(3, 'meet_emperor', 7, 1, 'respectful'),
|
||||||
|
(3, 'meet_emperor', 8, 1, 'curious'),
|
||||||
|
(3, 'meet_consort', 7, 1, 'cautious'),
|
||||||
|
(3, 'meet_consort', 9, 1, 'scrutinizing'),
|
||||||
|
(3, 'choice_emperor', 7, 1, 'humble'),
|
||||||
|
(3, 'choice_emperor', 8, 1, 'pleased'),
|
||||||
|
(3, 'choice_queen', 7, 1, 'calculating'),
|
||||||
|
(3, 'choice_queen', 9, 1, 'satisfied'),
|
||||||
|
(3, 'ending_empress', 7, 0, 'triumphant'),
|
||||||
|
(3, 'ending_empress', 8, 1, 'loving'),
|
||||||
|
(3, 'ending_concubine', 7, 0, 'peaceful'),
|
||||||
|
(3, 'ending_tragic', 7, 0, 'despairing');
|
||||||
|
|
||||||
|
-- ============================================
|
||||||
|
-- 故事4: 暗恋那件小事
|
||||||
|
-- ============================================
|
||||||
|
INSERT INTO story_characters (id, story_id, name, role_type, gender, age_range, appearance, personality, avatar_prompt) VALUES
|
||||||
|
(10, 4, '你', 'protagonist', 'female', 'teen', '扎着马尾的高中女生,青涩可爱,眼神里藏着小秘密', '内向害羞,暗恋成痴,纯真善良', 'teenage chinese girl, ponytail, school uniform, shy expression, cute appearance'),
|
||||||
|
(11, 4, '沈昼', 'protagonist', 'male', 'teen', '校园男神,阳光帅气,篮球服下身材修长,笑起来眼睛弯弯', '温柔体贴,学霸气质,暖心细腻', 'handsome teenage chinese boy, school uniform, basketball jersey, warm smile, athletic build'),
|
||||||
|
(12, 4, '同桌', 'supporting', 'female', 'teen', '活泼开朗的女生,表情丰富,是个话痨', '热心肠,爱八卦,仗义', 'cheerful teenage chinese girl, expressive face, school uniform, lively personality');
|
||||||
|
|
||||||
|
INSERT INTO node_characters (story_id, node_key, character_id, is_speaker, emotion) VALUES
|
||||||
|
(4, 'start', 10, 0, 'daydreaming'),
|
||||||
|
(4, 'start', 11, 0, 'neutral'),
|
||||||
|
(4, 'start', 12, 1, 'teasing'),
|
||||||
|
(4, 'library', 10, 1, 'flustered'),
|
||||||
|
(4, 'library', 11, 1, 'gentle'),
|
||||||
|
(4, 'basketball', 10, 0, 'excited'),
|
||||||
|
(4, 'basketball', 11, 1, 'playful'),
|
||||||
|
(4, 'rooftop', 10, 0, 'nervous'),
|
||||||
|
(4, 'rooftop', 11, 1, 'serious'),
|
||||||
|
(4, 'confess_yes', 10, 1, 'brave'),
|
||||||
|
(4, 'confess_yes', 11, 0, 'happy'),
|
||||||
|
(4, 'confess_no', 10, 0, 'tearful'),
|
||||||
|
(4, 'ending_friends', 10, 0, 'regretful');
|
||||||
|
|
||||||
|
-- ============================================
|
||||||
|
-- 故事5: 废柴逆袭录
|
||||||
|
-- ============================================
|
||||||
|
INSERT INTO story_characters (id, story_id, name, role_type, gender, age_range, appearance, personality, avatar_prompt) VALUES
|
||||||
|
(13, 5, '陈风', 'protagonist', 'male', 'young', '相貌普通的少年,眼神坚定,穿着破旧的外门弟子服', '隐忍不屈,心性坚韧,大智若愚', 'young chinese man, ordinary appearance, determined eyes, worn-out cultivation robes, underdog aura'),
|
||||||
|
(14, 5, '天元大帝', 'supporting', 'male', 'old', '白发苍苍的虚影,仙风道骨,眼中闪烁着智慧的光芒', '睿智从容,亦师亦友', 'ancient chinese immortal, white long hair, ethereal figure, wise eyes, flowing robes, ghostly appearance'),
|
||||||
|
(15, 5, '赵天龙', 'antagonist', 'male', 'young', '内门弟子,华服锦衣,面带傲色,眼高于顶', '嚣张跋扈,欺软怕硬', 'arrogant young chinese man, luxurious cultivation robes, haughty expression, inner sect disciple');
|
||||||
|
|
||||||
|
INSERT INTO node_characters (story_id, node_key, character_id, is_speaker, emotion) VALUES
|
||||||
|
(5, 'start', 13, 0, 'oppressed'),
|
||||||
|
(5, 'inheritance', 13, 0, 'shocked'),
|
||||||
|
(5, 'inheritance', 14, 1, 'wise'),
|
||||||
|
(5, 'challenge', 13, 0, 'calm'),
|
||||||
|
(5, 'challenge', 15, 1, 'mocking'),
|
||||||
|
(5, 'show_power', 13, 1, 'confident'),
|
||||||
|
(5, 'show_power', 15, 0, 'terrified'),
|
||||||
|
(5, 'hide_power', 13, 0, 'scheming'),
|
||||||
|
(5, 'hide_power', 14, 1, 'approving'),
|
||||||
|
(5, 'ending_immortal', 13, 0, 'transcendent'),
|
||||||
|
(5, 'ending_mortal', 13, 0, 'peaceful');
|
||||||
|
|
||||||
|
-- ============================================
|
||||||
|
-- 故事6: 回到高考前一天
|
||||||
|
-- ============================================
|
||||||
|
INSERT INTO story_characters (id, story_id, name, role_type, gender, age_range, appearance, personality, avatar_prompt) VALUES
|
||||||
|
(16, 6, '你', 'protagonist', 'male', 'teen', '穿着高中校服的少年,眼神带着超越年龄的沧桑', '成熟稳重,心怀遗憾,想要改变', 'teenage chinese boy, school uniform, eyes with wisdom beyond age, determined expression'),
|
||||||
|
(17, 6, '林小雨', 'supporting', 'female', 'teen', '瘦弱的女孩,眼眶微红,神情木然', '敏感脆弱,承受着巨大压力', 'fragile teenage chinese girl, red-rimmed eyes, blank expression, thin figure, school uniform'),
|
||||||
|
(18, 6, '爷爷', 'supporting', 'male', 'old', '慈祥的老人,满头银发,笑容温暖', '和蔼可亲,疼爱孙辈', 'kind elderly chinese man, silver hair, warm smile, traditional chinese clothes');
|
||||||
|
|
||||||
|
INSERT INTO node_characters (story_id, node_key, character_id, is_speaker, emotion) VALUES
|
||||||
|
(6, 'start', 16, 0, 'shocked'),
|
||||||
|
(6, 'choice_study', 16, 0, 'focused'),
|
||||||
|
(6, 'choice_relax', 16, 0, 'thoughtful'),
|
||||||
|
(6, 'visit_girl', 16, 1, 'concerned'),
|
||||||
|
(6, 'visit_girl', 17, 1, 'vulnerable'),
|
||||||
|
(6, 'ending_perfect', 16, 0, 'fulfilled'),
|
||||||
|
(6, 'ending_changed', 16, 0, 'content'),
|
||||||
|
(6, 'ending_changed', 17, 1, 'grateful');
|
||||||
|
|
||||||
|
-- ============================================
|
||||||
|
-- 故事7: 逆风翻盘
|
||||||
|
-- ============================================
|
||||||
|
INSERT INTO story_characters (id, story_id, name, role_type, gender, age_range, appearance, personality, avatar_prompt) VALUES
|
||||||
|
(19, 7, '陈墨', 'protagonist', 'male', 'young', '改头换面后的复仇者,西装革履,眼神锐利如鹰', '隐忍多年,城府极深,复仇心切', 'handsome chinese businessman, sharp suit, eagle-like eyes, confident posture, mysterious aura'),
|
||||||
|
(20, 7, '周明', 'antagonist', 'male', 'middle', '发福的中年男人,眼神世故贪婪,穿着名牌却掩不住小人嘴脸', '背信弃义,贪婪无耻', 'overweight chinese businessman, greedy eyes, expensive suit, treacherous appearance');
|
||||||
|
|
||||||
|
INSERT INTO node_characters (story_id, node_key, character_id, is_speaker, emotion) VALUES
|
||||||
|
(7, 'start', 19, 0, 'vengeful'),
|
||||||
|
(7, 'meeting', 19, 1, 'calculating'),
|
||||||
|
(7, 'meeting', 20, 1, 'obsequious'),
|
||||||
|
(7, 'expose', 19, 1, 'revealing'),
|
||||||
|
(7, 'expose', 20, 1, 'terrified'),
|
||||||
|
(7, 'ending_revenge', 19, 0, 'empty'),
|
||||||
|
(7, 'ending_forgive', 19, 0, 'liberated');
|
||||||
|
|
||||||
|
-- ============================================
|
||||||
|
-- 故事8: 2099最后一班地铁
|
||||||
|
-- ============================================
|
||||||
|
INSERT INTO story_characters (id, story_id, name, role_type, gender, age_range, appearance, personality, avatar_prompt) VALUES
|
||||||
|
(21, 8, '你', 'protagonist', 'male', 'young', '末世的普通人,穿着简朴,眼神迷茫又带着一丝希望', '迷茫困惑,渴望改变', 'young man in futuristic simple clothes, contemplative expression, sci-fi subway setting'),
|
||||||
|
(22, 8, 'Zero', 'supporting', 'female', 'young', '穿白裙的神秘女孩,眼睛明亮,不属于这个时代', '超然世外,引导者', 'mysterious girl in white dress, bright luminous eyes, ethereal beauty, futuristic subway conductor');
|
||||||
|
|
||||||
|
INSERT INTO node_characters (story_id, node_key, character_id, is_speaker, emotion) VALUES
|
||||||
|
(8, 'start', 21, 0, 'curious'),
|
||||||
|
(8, 'start', 22, 1, 'welcoming'),
|
||||||
|
(8, 'talk_girl', 21, 1, 'questioning'),
|
||||||
|
(8, 'talk_girl', 22, 1, 'mysterious'),
|
||||||
|
(8, 'choose_past', 21, 1, 'hopeful'),
|
||||||
|
(8, 'choose_past', 22, 1, 'guiding'),
|
||||||
|
(8, 'ending_past', 21, 0, 'determined'),
|
||||||
|
(8, 'ending_past', 22, 1, 'encouraging'),
|
||||||
|
(8, 'ending_future', 21, 0, 'amazed'),
|
||||||
|
(8, 'ending_future', 22, 1, 'serene');
|
||||||
|
|
||||||
|
-- ============================================
|
||||||
|
-- 故事9: 第七夜
|
||||||
|
-- ============================================
|
||||||
|
INSERT INTO story_characters (id, story_id, name, role_type, gender, age_range, appearance, personality, avatar_prompt) VALUES
|
||||||
|
(23, 9, '你', 'protagonist', 'male', 'young', '被困旅馆的旅客,神情紧张,黑眼圈明显', '胆小谨慎,求生欲强', 'nervous young man, dark circles under eyes, casual travel clothes, fearful expression'),
|
||||||
|
(24, 9, '老板娘', 'supporting', 'female', 'middle', '面容苍老,眼神奇怪,穿着老旧的旗袍', '神秘莫测,知道真相', 'mysterious middle-aged chinese woman, old-fashioned qipao, strange knowing eyes, creepy inn keeper'),
|
||||||
|
(25, 9, '白裙女孩', 'antagonist', 'female', 'young', '苍白的脸,穿着白裙,笑容诡异,若隐若现', '怨念深重,寻找陪伴', 'pale ghost girl in white dress, eerie smile, translucent figure, long black hair covering face, horror style');
|
||||||
|
|
||||||
|
INSERT INTO node_characters (story_id, node_key, character_id, is_speaker, emotion) VALUES
|
||||||
|
(9, 'start', 23, 0, 'uneasy'),
|
||||||
|
(9, 'start', 24, 1, 'cryptic'),
|
||||||
|
(9, 'night_sound', 23, 0, 'terrified'),
|
||||||
|
(9, 'night_sound', 25, 1, 'pleading'),
|
||||||
|
(9, 'open_door', 23, 0, 'horrified'),
|
||||||
|
(9, 'open_door', 25, 1, 'sinister'),
|
||||||
|
(9, 'not_open', 23, 0, 'anxious'),
|
||||||
|
(9, 'ending_death', 23, 0, 'dying'),
|
||||||
|
(9, 'ending_death', 25, 1, 'satisfied'),
|
||||||
|
(9, 'ending_escape', 23, 0, 'relieved');
|
||||||
|
|
||||||
|
-- ============================================
|
||||||
|
-- 故事10: 我的室友是只猫
|
||||||
|
-- ============================================
|
||||||
|
INSERT INTO story_characters (id, story_id, name, role_type, gender, age_range, appearance, personality, avatar_prompt) VALUES
|
||||||
|
(26, 10, '你', 'protagonist', 'male', 'young', '普通大学生,一脸懵逼,被室友变猫吓到', '善良搞笑,容易慌张', 'confused chinese college student, dorm room setting, panicked expression, casual clothes'),
|
||||||
|
(27, 10, '室友/橘猫', 'protagonist', 'male', 'young', '胖胖的橘色大猫,会说话,表情丰富,原本是个学霸', '聪明话多,倒霉体质,吃货', 'fat orange tabby cat, expressive face, sitting like human, funny expression, college dorm background');
|
||||||
|
|
||||||
|
INSERT INTO node_characters (story_id, node_key, character_id, is_speaker, emotion) VALUES
|
||||||
|
(10, 'start', 26, 0, 'shocked'),
|
||||||
|
(10, 'start', 27, 1, 'frustrated'),
|
||||||
|
(10, 'panic', 26, 1, 'panicking'),
|
||||||
|
(10, 'panic', 27, 1, 'embarrassed'),
|
||||||
|
(10, 'exam_plan', 26, 0, 'thinking'),
|
||||||
|
(10, 'exam_plan', 27, 1, 'excited'),
|
||||||
|
(10, 'bring_cat', 26, 0, 'nervous'),
|
||||||
|
(10, 'bring_cat', 27, 1, 'whispering'),
|
||||||
|
(10, 'ending_funny', 26, 0, 'relieved'),
|
||||||
|
(10, 'ending_funny', 27, 1, 'smug'),
|
||||||
|
(10, 'ending_caught', 26, 0, 'doomed'),
|
||||||
|
(10, 'ending_caught', 27, 1, 'defeated');
|
||||||
Reference in New Issue
Block a user