Files
cultivation-world-simulator/src/classes/story_teller.py
2025-11-19 01:06:42 +08:00

155 lines
6.1 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.
from __future__ import annotations
from typing import Dict, TYPE_CHECKING
import asyncio
import random
from src.utils.config import CONFIG
from src.utils.llm import get_prompt_and_call_llm, get_prompt_and_call_llm_async
story_styles = [
"平淡叙述:语句克制、少修饰、像旁观者记录。",
"寓情于景:以景见情,情随境迁,情景交融。",
"写意古风:重意象与比兴,点到为止,少生僻文言。",
"市井烟火:接地气,含些俗语但不粗鄙,烟火气足。",
"悬疑铺垫:埋伏笔与反转,信息递进,结尾留一丝余味。",
"诗意抒情:短句与对仗点缀,少量用典,不堆砌辞藻。",
"哲思寓言:借事设问,含一两句点睛之语,不说教。",
"黑色幽默:以反差与轻描淡写呈现荒诞,克制机锋。",
"编年纪事:近史官笔法,记事有序,少形容词。",
"碎片蒙太奇:并置数个短镜头,以意连形,留白。",
"景物拟人:对景施以轻微拟人,景中含志,不滥。",
"道法自然:以道家语汇点染,不艰涩,收束于一念。",
"佛理空相:无常、空相的领悟穿插事中,轻淡不玄。",
"民间说书:似说书人口吻但用书面语,收尾有眼。",
"雅致书卷:书卷气、引文气息浅尝辄止,不显摆。",
]
class StoryTeller:
"""
故事生成器:基于模板与 LLM将给定事件扩展为简短的小故事。
"""
@staticmethod
def build_avatar_infos(*avatars: "Avatar") -> Dict[str, dict]:
"""
将若干角色信息组织为 {name: info_dict} 映射,供故事模板使用。
战斗/小故事使用详细信息dict 版)。
"""
infos: Dict[str, dict] = {}
for av in avatars:
if av is None:
continue
infos[av.name] = av.get_info(detailed=True)
return infos
@staticmethod
def tell_story(event: str, res: str, *actors: "Avatar", prompt: str = "") -> str:
"""
基于 `static/templates/story.txt` 模板生成小故事。
始终使用 fast 模式以提升速度。
失败时返回降级版文案,避免中断流程。
Args:
event: 事件描述
res: 结果描述
*actors: 参与的角色1-2个
prompt: 可选的故事提示词
"""
# 构建 avatar_infos第一个 avatar 使用 expanded_info
non_null = [a for a in actors if a is not None]
avatar_infos: Dict[str, dict] = {}
if len(non_null) >= 2:
# 双人故事:第一个用 expanded_info包含共同事件第二个用 detailed info
avatar_infos[non_null[0].name] = non_null[0].get_expanded_info(other_avatar=non_null[1], detailed=True)
avatar_infos[non_null[1].name] = non_null[1].get_info(detailed=True)
elif non_null:
# 单人故事:直接用 expanded_info
avatar_infos[non_null[0].name] = non_null[0].get_expanded_info(detailed=True)
template_path = CONFIG.paths.templates / "story.txt"
infos = {
"avatar_infos": avatar_infos,
"event": event,
"res": res,
"style": random.choice(story_styles),
"story_prompt": prompt,
}
try:
data = get_prompt_and_call_llm(template_path, infos, mode="fast")
story = data.get("story", "").strip()
if story:
return story
except Exception:
pass
# 降级文案(不中断主流程)
style = infos.get("style", "")
return f"{event}{res}{style}"
@staticmethod
async def tell_story_async(event: str, res: str, *actors: "Avatar", prompt: str = "") -> str:
"""
异步版本:生成小故事,失败时返回降级文案。
Args:
event: 事件描述
res: 结果描述
*actors: 参与的角色1-2个
prompt: 可选的故事提示词
"""
# 构建 avatar_infos第一个 avatar 使用 expanded_info
non_null = [a for a in actors if a is not None]
avatar_infos: Dict[str, dict] = {}
if len(non_null) >= 2:
# 双人故事:第一个用 expanded_info包含共同事件第二个用 detailed info
avatar_infos[non_null[0].name] = non_null[0].get_expanded_info(other_avatar=non_null[1], detailed=True)
avatar_infos[non_null[1].name] = non_null[1].get_info(detailed=True)
elif non_null:
# 单人故事:直接用 expanded_info
avatar_infos[non_null[0].name] = non_null[0].get_expanded_info(detailed=True)
template_path = CONFIG.paths.templates / "story.txt"
infos = {
"avatar_infos": avatar_infos,
"event": event,
"res": res,
"style": random.choice(story_styles),
"story_prompt": prompt,
}
try:
data = await get_prompt_and_call_llm_async(template_path, infos, mode="fast")
story = str(data.get("story", "")).strip()
if story:
return story
except Exception:
pass
style = infos.get("style", "")
return f"{event}{res}{style}"
@staticmethod
def tell_from_actors(event: str, res: str, *actors: "Avatar", prompt: str | None = None) -> str:
"""
便捷方法别名,保持向后兼容。直接调用 tell_story。
"""
return StoryTeller.tell_story(event, res, *actors, prompt=prompt or "")
@staticmethod
async def tell_from_actors_async(event: str, res: str, *actors: "Avatar", prompt: str | None = None) -> str:
"""
便捷方法别名,保持向后兼容。直接调用 tell_story_async。
"""
return await StoryTeller.tell_story_async(event, res, *actors, prompt=prompt or "")
__all__ = ["StoryTeller"]
if TYPE_CHECKING:
# 仅用于类型检查,避免循环导入
from src.classes.avatar import Avatar