155 lines
6.1 KiB
Python
155 lines
6.1 KiB
Python
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
|
||
|