From c4bc8daddc6aa9f85aef444221a3ca0798e7177e Mon Sep 17 00:00:00 2001 From: bridge Date: Wed, 19 Nov 2025 01:13:44 +0800 Subject: [PATCH] refactor story teller --- src/classes/action/battle.py | 2 +- src/classes/action/breakthrough.py | 2 +- src/classes/fortune.py | 2 +- src/classes/mutual_action/dual_cultivation.py | 4 +- .../mutual_action/gift_spirit_stone.py | 2 +- src/classes/mutual_action/impart.py | 2 +- src/classes/story_teller.py | 129 +++++++----------- 7 files changed, 54 insertions(+), 89 deletions(-) diff --git a/src/classes/action/battle.py b/src/classes/action/battle.py index 9215c24..e7415e4 100644 --- a/src/classes/action/battle.py +++ b/src/classes/action/battle.py @@ -90,7 +90,7 @@ class Battle(InstantAction): # 生成战斗小故事(同步调用,与其他动作保持一致) target = self._get_target(avatar_name) start_text = self._start_event_content if hasattr(self, '_start_event_content') else result_event.content - story = StoryTeller.tell_from_actors(start_text, result_event.content, self.avatar, target, prompt=self.STORY_PROMPT) + story = StoryTeller.tell_story(start_text, result_event.content, self.avatar, target, prompt=self.STORY_PROMPT) story_event = Event(self.world.month_stamp, story, related_avatars=rel_ids, is_story=True) return [result_event, story_event] diff --git a/src/classes/action/breakthrough.py b/src/classes/action/breakthrough.py index f13a6f5..5830a59 100644 --- a/src/classes/action/breakthrough.py +++ b/src/classes/action/breakthrough.py @@ -139,7 +139,7 @@ class Breakthrough(TimedAction): # 故事参与者:本体 +(可选)相关角色 prompt = TribulationSelector.get_story_prompt(str(calamity)) - story = StoryTeller.tell_from_actors(core_text, ("突破成功" if result_ok else "突破失败"), self.avatar, self._calamity_other, prompt=prompt) + story = StoryTeller.tell_story(core_text, ("突破成功" if result_ok else "突破失败"), self.avatar, self._calamity_other, prompt=prompt) events.append(Event(self.world.month_stamp, story, related_avatars=rel_ids, is_story=True)) return events diff --git a/src/classes/fortune.py b/src/classes/fortune.py index b39bdc2..e88a4ef 100644 --- a/src/classes/fortune.py +++ b/src/classes/fortune.py @@ -478,7 +478,7 @@ async def try_trigger_fortune(avatar: Avatar) -> list[Event]: base_event = Event(month_at_finish, event_text, related_avatars=related_avatars, is_major=True) # 生成故事事件 - story = await StoryTeller.tell_from_actors_async(event_text, res_text, *actors_for_story, prompt=story_prompt) + story = await StoryTeller.tell_story_async(event_text, res_text, *actors_for_story, prompt=story_prompt) story_event = Event(month_at_finish, story, related_avatars=related_avatars, is_story=True) # 返回基础事件和故事事件 diff --git a/src/classes/mutual_action/dual_cultivation.py b/src/classes/mutual_action/dual_cultivation.py index 7d9b2b5..2d50876 100644 --- a/src/classes/mutual_action/dual_cultivation.py +++ b/src/classes/mutual_action/dual_cultivation.py @@ -109,9 +109,9 @@ class DualCultivation(MutualAction): result_event = Event(self.world.month_stamp, result_text, related_avatars=[self.avatar.id, target.id], is_major=True) events.append(result_event) - # 生成恋爱/双修小故事:使用 StoryTeller 便捷方法 + # 生成恋爱/双修小故事 start_text = self._start_event_content or result_event.content - story = StoryTeller.tell_from_actors(start_text, result_event.content, self.avatar, target, prompt=self.STORY_PROMPT) + story = StoryTeller.tell_story(start_text, result_event.content, self.avatar, target, prompt=self.STORY_PROMPT) story_event = Event(self.world.month_stamp, story, related_avatars=[self.avatar.id, target.id], is_story=True) events.append(story_event) else: diff --git a/src/classes/mutual_action/gift_spirit_stone.py b/src/classes/mutual_action/gift_spirit_stone.py index 2fe073d..2080ad4 100644 --- a/src/classes/mutual_action/gift_spirit_stone.py +++ b/src/classes/mutual_action/gift_spirit_stone.py @@ -98,7 +98,7 @@ class GiftSpiritStone(MutualAction): # 生成赠送小故事 from src.classes.story_teller import StoryTeller start_text = self._start_event_content or result_event.content - story = StoryTeller.tell_from_actors( + story = StoryTeller.tell_story( start_text, result_text, self.avatar, diff --git a/src/classes/mutual_action/impart.py b/src/classes/mutual_action/impart.py index 7d75afd..439eaf3 100644 --- a/src/classes/mutual_action/impart.py +++ b/src/classes/mutual_action/impart.py @@ -110,7 +110,7 @@ class Impart(MutualAction): # 生成师徒传道小故事 from src.classes.story_teller import StoryTeller start_text = self._start_event_content or result_event.content - story = StoryTeller.tell_from_actors( + story = StoryTeller.tell_story( start_text, result_text, self.avatar, diff --git a/src/classes/story_teller.py b/src/classes/story_teller.py index a99971a..06b9f64 100644 --- a/src/classes/story_teller.py +++ b/src/classes/story_teller.py @@ -1,7 +1,6 @@ from __future__ import annotations from typing import Dict, TYPE_CHECKING -import asyncio import random from src.utils.config import CONFIG @@ -30,26 +29,48 @@ class StoryTeller: """ 故事生成器:基于模板与 LLM,将给定事件扩展为简短的小故事。 """ + + TEMPLATE_PATH = CONFIG.paths.templates / "story.txt" @staticmethod - def build_avatar_infos(*avatars: "Avatar") -> Dict[str, dict]: + def _build_avatar_infos(*actors: "Avatar") -> Dict[str, dict]: """ - 将若干角色信息组织为 {name: info_dict} 映射,供故事模板使用。 - 战斗/小故事使用详细信息(dict 版)。 + 构建角色信息字典。 + - 双人故事:第一个角色使用 expanded_info(包含共同事件),第二个使用普通 info + - 单人故事:使用 expanded_info """ - infos: Dict[str, dict] = {} - for av in avatars: - if av is None: - continue - infos[av.name] = av.get_info(detailed=True) - return infos + non_null = [a for a in actors if a is not None] + avatar_infos: Dict[str, dict] = {} + + if len(non_null) >= 2: + 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: + avatar_infos[non_null[0].name] = non_null[0].get_expanded_info(detailed=True) + + return avatar_infos + + @staticmethod + def _build_template_data(event: str, res: str, avatar_infos: Dict[str, dict], prompt: str) -> dict: + """构建模板渲染所需的数据字典""" + return { + "avatar_infos": avatar_infos, + "event": event, + "res": res, + "style": random.choice(story_styles), + "story_prompt": prompt, + } + + @staticmethod + def _make_fallback_story(event: str, res: str, style: str) -> str: + """生成降级文案""" + return f"{event}。{res}。{style}" @staticmethod def tell_story(event: str, res: str, *actors: "Avatar", prompt: str = "") -> str: """ - 基于 `static/templates/story.txt` 模板生成小故事。 - 始终使用 fast 模式以提升速度。 - 失败时返回降级版文案,避免中断流程。 + 生成小故事(同步版本)。 + 基于 `static/templates/story.txt` 模板,失败时返回降级文案。 Args: event: 事件描述 @@ -57,41 +78,24 @@ class StoryTeller: *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] = {} + avatar_infos = StoryTeller._build_avatar_infos(*actors) + infos = StoryTeller._build_template_data(event, res, avatar_infos, prompt) - 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") + data = get_prompt_and_call_llm(StoryTeller.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}" + + return StoryTeller._make_fallback_story(event, res, infos["style"]) @staticmethod async def tell_story_async(event: str, res: str, *actors: "Avatar", prompt: str = "") -> str: """ - 异步版本:生成小故事,失败时返回降级文案。 + 生成小故事(异步版本)。 + 基于 `static/templates/story.txt` 模板,失败时返回降级文案。 Args: event: 事件描述 @@ -99,56 +103,17 @@ class StoryTeller: *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] = {} + avatar_infos = StoryTeller._build_avatar_infos(*actors) + infos = StoryTeller._build_template_data(event, res, avatar_infos, prompt) - 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") + data = await get_prompt_and_call_llm_async(StoryTeller.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 + + return StoryTeller._make_fallback_story(event, res, infos["style"]) +__all__ = ["StoryTeller"] \ No newline at end of file