601 lines
19 KiB
Python
601 lines
19 KiB
Python
|
|
"""
|
|||
|
|
Fanfic Writer v2.0 - Phase Runner (Phases 1-5)
|
|||
|
|
Orchestrates initialization through worldbuilding
|
|||
|
|
"""
|
|||
|
|
import json
|
|||
|
|
import os
|
|||
|
|
from pathlib import Path
|
|||
|
|
from typing import Dict, Any, Optional, Tuple, Callable
|
|||
|
|
from datetime import datetime
|
|||
|
|
|
|||
|
|
from .workspace import WorkspaceManager, generate_intent_checklist
|
|||
|
|
from .config_manager import ConfigManager
|
|||
|
|
from .state_manager import StateManager
|
|||
|
|
from .prompt_registry import PromptRegistry
|
|||
|
|
from .price_table import PriceTableManager, CostBudgetManager
|
|||
|
|
from .resume_manager import RunLock, ResumeManager, RuntimeConfigManager
|
|||
|
|
from .atomic_io import atomic_write_json, atomic_write_text
|
|||
|
|
from .utils import get_timestamp_iso
|
|||
|
|
|
|||
|
|
|
|||
|
|
class PhaseRunner:
|
|||
|
|
"""
|
|||
|
|
Runs Phases 1-5 of the novel writing pipeline:
|
|||
|
|
- Phase 1: Initialization (config, workspace)
|
|||
|
|
- Phase 2: Style Guide
|
|||
|
|
- Phase 3: Main Outline
|
|||
|
|
- Phase 4: Chapter Planning
|
|||
|
|
- Phase 5: World Building
|
|||
|
|
- Phase 5.5: Alignment Check
|
|||
|
|
"""
|
|||
|
|
|
|||
|
|
def __init__(
|
|||
|
|
self,
|
|||
|
|
workspace_manager: WorkspaceManager,
|
|||
|
|
model_callable: Optional[Callable] = None
|
|||
|
|
):
|
|||
|
|
self.workspace = workspace_manager
|
|||
|
|
self.model_callable = model_callable
|
|||
|
|
self._current_run_dir: Optional[Path] = None
|
|||
|
|
self._config_manager: Optional[ConfigManager] = None
|
|||
|
|
self._state_manager: Optional[StateManager] = None
|
|||
|
|
|
|||
|
|
# =========================================================================
|
|||
|
|
# Phase 1: Initialization
|
|||
|
|
# =========================================================================
|
|||
|
|
|
|||
|
|
def phase1_initialize(
|
|||
|
|
self,
|
|||
|
|
book_title: str,
|
|||
|
|
genre: str,
|
|||
|
|
target_words: int,
|
|||
|
|
**kwargs
|
|||
|
|
) -> Tuple[Path, str, str]:
|
|||
|
|
"""
|
|||
|
|
Phase 1: Initialize book workspace
|
|||
|
|
|
|||
|
|
Returns:
|
|||
|
|
(run_dir, book_uid, run_id)
|
|||
|
|
"""
|
|||
|
|
print(f"[Phase 1] Initializing: {book_title}")
|
|||
|
|
|
|||
|
|
# Create workspace
|
|||
|
|
run_dir, book_uid, run_id, paths = self.workspace.create_new_book(
|
|||
|
|
book_title=book_title,
|
|||
|
|
genre=genre,
|
|||
|
|
target_words=target_words,
|
|||
|
|
**kwargs
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
self._current_run_dir = run_dir
|
|||
|
|
|
|||
|
|
# Initialize config manager
|
|||
|
|
self._config_manager = ConfigManager(run_dir)
|
|||
|
|
config = self._config_manager.load()
|
|||
|
|
|
|||
|
|
# Generate intent checklist
|
|||
|
|
checklist = generate_intent_checklist(config)
|
|||
|
|
checklist_path = paths['intent_checklist']
|
|||
|
|
atomic_write_json(checklist_path, checklist)
|
|||
|
|
|
|||
|
|
# Initialize price table
|
|||
|
|
price_mgr = PriceTableManager(run_dir)
|
|||
|
|
usd_cny_rate = kwargs.get('usd_cny_rate', 6.90)
|
|||
|
|
price_mgr.initialize(usd_cny_rate=usd_cny_rate)
|
|||
|
|
|
|||
|
|
# Acquire run lock
|
|||
|
|
run_lock = RunLock(run_dir)
|
|||
|
|
lock_success, lock_error = run_lock.acquire(mode=kwargs.get('mode', 'manual'))
|
|||
|
|
if not lock_success:
|
|||
|
|
raise RuntimeError(f"Cannot acquire run lock: {lock_error}")
|
|||
|
|
|
|||
|
|
# Generate runtime effective config
|
|||
|
|
rt_mgr = RuntimeConfigManager(run_dir)
|
|||
|
|
rt_mgr.generate(
|
|||
|
|
cli_args={k: v for k, v in kwargs.items() if v is not None},
|
|||
|
|
env_vars={k: v for k, v in os.environ.items() if k.startswith('FANFIC_')},
|
|||
|
|
config_file={'mode': config['generation']['mode'],
|
|||
|
|
'model': config['generation']['model']},
|
|||
|
|
defaults={'max_attempts': 3, 'auto_threshold': 85}
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
print(f"[Phase 1] Complete: {run_dir}")
|
|||
|
|
return run_dir, book_uid, run_id
|
|||
|
|
|
|||
|
|
# =========================================================================
|
|||
|
|
# Phase 2: Style Guide
|
|||
|
|
# =========================================================================
|
|||
|
|
|
|||
|
|
def phase2_style_guide(
|
|||
|
|
self,
|
|||
|
|
narrative_voice: str = "第三人称限知",
|
|||
|
|
dialogue_style: str = "口语化",
|
|||
|
|
description_density: str = "动作>心理>环境",
|
|||
|
|
humor_tension_balance: str = "70%轻松+30%紧张",
|
|||
|
|
custom_rules: Optional[list] = None
|
|||
|
|
) -> Path:
|
|||
|
|
"""
|
|||
|
|
Phase 2: Generate style guide
|
|||
|
|
|
|||
|
|
Can be auto-generated or use provided values
|
|||
|
|
"""
|
|||
|
|
print("[Phase 2] Generating style guide...")
|
|||
|
|
|
|||
|
|
if not self._current_run_dir:
|
|||
|
|
raise RuntimeError("Must run Phase 1 first")
|
|||
|
|
|
|||
|
|
style_guide_path = self._current_run_dir / "0-config" / "style_guide.md"
|
|||
|
|
|
|||
|
|
# Build style guide content
|
|||
|
|
content_lines = [
|
|||
|
|
f"# Style Guide",
|
|||
|
|
"",
|
|||
|
|
f"## Narrative Voice",
|
|||
|
|
f"{narrative_voice}",
|
|||
|
|
"",
|
|||
|
|
f"## Dialogue Style",
|
|||
|
|
f"{dialogue_style}",
|
|||
|
|
"",
|
|||
|
|
f"## Description Density",
|
|||
|
|
f"{description_density}",
|
|||
|
|
"",
|
|||
|
|
f"## Humor/Tension Balance",
|
|||
|
|
f"{humor_tension_balance}",
|
|||
|
|
"",
|
|||
|
|
"## Forbidden Phrases",
|
|||
|
|
"- \"突然\" (用\"猛然\"或具体动作代替)",
|
|||
|
|
"- \"非常\"\"特别\" (用具体描写代替)",
|
|||
|
|
"- 连续超过3句对话无动作/心理描写",
|
|||
|
|
]
|
|||
|
|
|
|||
|
|
if custom_rules:
|
|||
|
|
content_lines.extend(["", "## Custom Rules"])
|
|||
|
|
for rule in custom_rules:
|
|||
|
|
content_lines.append(f"- {rule}")
|
|||
|
|
|
|||
|
|
content = "\n".join(content_lines)
|
|||
|
|
atomic_write_text(style_guide_path, content)
|
|||
|
|
|
|||
|
|
print(f"[Phase 2] Complete: {style_guide_path}")
|
|||
|
|
return style_guide_path
|
|||
|
|
|
|||
|
|
# =========================================================================
|
|||
|
|
# Phase 3: Main Outline
|
|||
|
|
# =========================================================================
|
|||
|
|
|
|||
|
|
def phase3_main_outline(
|
|||
|
|
self,
|
|||
|
|
outline_content: Optional[str] = None
|
|||
|
|
) -> Path:
|
|||
|
|
"""
|
|||
|
|
Phase 3: Generate or save main outline
|
|||
|
|
|
|||
|
|
If outline_content provided, save it directly.
|
|||
|
|
Otherwise, would call model (placeholder for future)
|
|||
|
|
"""
|
|||
|
|
print("[Phase 3] Main outline...")
|
|||
|
|
|
|||
|
|
if not self._current_run_dir:
|
|||
|
|
raise RuntimeError("Must run Phase 1 first")
|
|||
|
|
|
|||
|
|
outline_path = self._current_run_dir / "1-outline" / "1-main-outline.md"
|
|||
|
|
|
|||
|
|
if outline_content:
|
|||
|
|
# Use provided content
|
|||
|
|
atomic_write_text(outline_path, outline_content)
|
|||
|
|
else:
|
|||
|
|
# Placeholder: would call model
|
|||
|
|
# For now, create template
|
|||
|
|
config = self._config_manager.load()
|
|||
|
|
title = config['book']['title']
|
|||
|
|
genre = config['book']['genre']
|
|||
|
|
|
|||
|
|
template = f"""# {title} - 主线大纲
|
|||
|
|
|
|||
|
|
## 题材
|
|||
|
|
{genre}
|
|||
|
|
|
|||
|
|
## 一句话简介
|
|||
|
|
(待填写:20字内核心卖点)
|
|||
|
|
|
|||
|
|
## 核心卖点
|
|||
|
|
- 卖点1:...
|
|||
|
|
- 卖点2:...
|
|||
|
|
- 卖点3:...
|
|||
|
|
|
|||
|
|
## 世界背景
|
|||
|
|
(待填写)
|
|||
|
|
|
|||
|
|
## 主要角色
|
|||
|
|
|
|||
|
|
### 主角
|
|||
|
|
- 姓名/代号:
|
|||
|
|
- 身份背景:
|
|||
|
|
- 性格特点:
|
|||
|
|
- 核心目标:
|
|||
|
|
|
|||
|
|
## 主线剧情
|
|||
|
|
|
|||
|
|
### 第一卷:【卷名】(第1-?章)
|
|||
|
|
卷主题:
|
|||
|
|
核心冲突:
|
|||
|
|
|
|||
|
|
### 第二卷:【卷名】
|
|||
|
|
...
|
|||
|
|
|
|||
|
|
## 关键转折点
|
|||
|
|
1. 第X章:...
|
|||
|
|
|
|||
|
|
## 预计完结
|
|||
|
|
(待填写)
|
|||
|
|
"""
|
|||
|
|
atomic_write_text(outline_path, template)
|
|||
|
|
|
|||
|
|
print(f"[Phase 3] Complete: {outline_path}")
|
|||
|
|
return outline_path
|
|||
|
|
|
|||
|
|
# =========================================================================
|
|||
|
|
# Phase 4: Chapter Planning
|
|||
|
|
# =========================================================================
|
|||
|
|
|
|||
|
|
def phase4_chapter_plan(
|
|||
|
|
self,
|
|||
|
|
chapters_data: Optional[list] = None
|
|||
|
|
) -> Tuple[Path, Path]:
|
|||
|
|
"""
|
|||
|
|
Phase 4: Generate chapter plan and detailed outlines index
|
|||
|
|
|
|||
|
|
Returns:
|
|||
|
|
(chapter_plan_path, chapter_outlines_path)
|
|||
|
|
"""
|
|||
|
|
print("[Phase 4] Chapter planning...")
|
|||
|
|
|
|||
|
|
if not self._current_run_dir:
|
|||
|
|
raise RuntimeError("Must run Phase 1 first")
|
|||
|
|
|
|||
|
|
config = self._config_manager.load()
|
|||
|
|
target_words = config['book']['target_word_count']
|
|||
|
|
chapter_target = config['book']['chapter_target_words']
|
|||
|
|
|
|||
|
|
# Calculate chapter count
|
|||
|
|
estimated_chapters = max(10, target_words // chapter_target)
|
|||
|
|
|
|||
|
|
if chapters_data:
|
|||
|
|
# Use provided data
|
|||
|
|
chapter_plan = {
|
|||
|
|
'total_chapters': len(chapters_data),
|
|||
|
|
'chapters': chapters_data
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
chapter_outlines = {
|
|||
|
|
'chapters': [
|
|||
|
|
{
|
|||
|
|
'chapter_number': c['number'],
|
|||
|
|
'title': c['title'],
|
|||
|
|
'theme': c.get('key_event', ''),
|
|||
|
|
'purpose': c.get('summary', ''),
|
|||
|
|
'word_count_target': c.get('target_words', chapter_target),
|
|||
|
|
'hook': c.get('cliffhanger', '')
|
|||
|
|
}
|
|||
|
|
for c in chapters_data
|
|||
|
|
]
|
|||
|
|
}
|
|||
|
|
else:
|
|||
|
|
# Create template structure
|
|||
|
|
chapter_plan = {
|
|||
|
|
'total_chapters': estimated_chapters,
|
|||
|
|
'chapters': [
|
|||
|
|
{
|
|||
|
|
'chapter_number': i,
|
|||
|
|
'title': f"第{i}章",
|
|||
|
|
'word_count': chapter_target,
|
|||
|
|
'pacing': 'medium',
|
|||
|
|
'tension': 'medium',
|
|||
|
|
'scene_breakdown': []
|
|||
|
|
}
|
|||
|
|
for i in range(1, min(estimated_chapters + 1, 101))
|
|||
|
|
],
|
|||
|
|
'volume_breakdown': [
|
|||
|
|
{'volume': 1, 'name': '第一卷', 'chapters': f'1-{min(20, estimated_chapters)}'}
|
|||
|
|
]
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
chapter_outlines = {
|
|||
|
|
'chapters': [
|
|||
|
|
{
|
|||
|
|
'chapter_number': i,
|
|||
|
|
'title': f"第{i}章",
|
|||
|
|
'theme': '待设定',
|
|||
|
|
'purpose': '待设定',
|
|||
|
|
'word_count_target': chapter_target,
|
|||
|
|
'scenes': [],
|
|||
|
|
'characters': [],
|
|||
|
|
'plot_points': [],
|
|||
|
|
'hook': ''
|
|||
|
|
}
|
|||
|
|
for i in range(1, min(estimated_chapters + 1, 101))
|
|||
|
|
]
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
# Save files
|
|||
|
|
plan_path = self._current_run_dir / "2-planning" / "2-chapter-plan.json"
|
|||
|
|
outlines_path = self._current_run_dir / "1-outline" / "5-chapter-outlines.json"
|
|||
|
|
|
|||
|
|
atomic_write_json(plan_path, chapter_plan)
|
|||
|
|
atomic_write_json(outlines_path, chapter_outlines)
|
|||
|
|
|
|||
|
|
print(f"[Phase 4] Complete: {plan_path}, {outlines_path}")
|
|||
|
|
return plan_path, outlines_path
|
|||
|
|
|
|||
|
|
# =========================================================================
|
|||
|
|
# Phase 5: World Building
|
|||
|
|
# =========================================================================
|
|||
|
|
|
|||
|
|
def phase5_world_building(
|
|||
|
|
self,
|
|||
|
|
world_content: Optional[str] = None
|
|||
|
|
) -> Path:
|
|||
|
|
"""
|
|||
|
|
Phase 5: Generate world building document
|
|||
|
|
"""
|
|||
|
|
print("[Phase 5] World building...")
|
|||
|
|
|
|||
|
|
if not self._current_run_dir:
|
|||
|
|
raise RuntimeError("Must run Phase 1 first")
|
|||
|
|
|
|||
|
|
world_path = self._current_run_dir / "3-world" / "3-world-building.md"
|
|||
|
|
|
|||
|
|
if world_content:
|
|||
|
|
atomic_write_text(world_path, world_content)
|
|||
|
|
else:
|
|||
|
|
# Create template
|
|||
|
|
config = self._config_manager.load()
|
|||
|
|
title = config['book']['title']
|
|||
|
|
|
|||
|
|
template = f"""# {title} - 世界观设定
|
|||
|
|
|
|||
|
|
## 世界观
|
|||
|
|
|
|||
|
|
### 时空背景
|
|||
|
|
- 时间:
|
|||
|
|
- 空间:
|
|||
|
|
- 基本规则:
|
|||
|
|
|
|||
|
|
### 势力分布
|
|||
|
|
(待填写)
|
|||
|
|
|
|||
|
|
### 力量/技能体系
|
|||
|
|
- 体系名称:
|
|||
|
|
- 等级划分:
|
|||
|
|
- 核心规则:
|
|||
|
|
- 限制条件:
|
|||
|
|
|
|||
|
|
## 主要角色
|
|||
|
|
|
|||
|
|
### 主角
|
|||
|
|
【基础信息】
|
|||
|
|
- 姓名:
|
|||
|
|
- 年龄:
|
|||
|
|
- 外貌特征:
|
|||
|
|
|
|||
|
|
【性格】
|
|||
|
|
- 表层性格:
|
|||
|
|
- 深层性格:
|
|||
|
|
- 性格缺陷:
|
|||
|
|
|
|||
|
|
【背景】
|
|||
|
|
- 出身:
|
|||
|
|
- 关键经历:
|
|||
|
|
- 关系网:
|
|||
|
|
|
|||
|
|
【目标与成长】
|
|||
|
|
- 短期目标:
|
|||
|
|
- 长期目标:
|
|||
|
|
- 成长弧线:
|
|||
|
|
|
|||
|
|
## 关键设定
|
|||
|
|
|
|||
|
|
### 重要道具
|
|||
|
|
...
|
|||
|
|
|
|||
|
|
### 重要地点
|
|||
|
|
...
|
|||
|
|
|
|||
|
|
### 关键规则
|
|||
|
|
...
|
|||
|
|
"""
|
|||
|
|
atomic_write_text(world_path, template)
|
|||
|
|
|
|||
|
|
print(f"[Phase 5] Complete: {world_path}")
|
|||
|
|
return world_path
|
|||
|
|
|
|||
|
|
# =========================================================================
|
|||
|
|
# Phase 5.5: Alignment Check
|
|||
|
|
# =========================================================================
|
|||
|
|
|
|||
|
|
def phase5_5_alignment_check(self) -> Tuple[float, Optional[Path]]:
|
|||
|
|
"""
|
|||
|
|
Phase 5.5: Check alignment between intent checklist and world building
|
|||
|
|
|
|||
|
|
Returns:
|
|||
|
|
(deviation_score, warning_path or None)
|
|||
|
|
deviation_score: 0.0 = perfect alignment, 1.0 = completely off
|
|||
|
|
"""
|
|||
|
|
print("[Phase 5.5] Alignment check...")
|
|||
|
|
|
|||
|
|
if not self._current_run_dir:
|
|||
|
|
raise RuntimeError("Must run Phase 1 first")
|
|||
|
|
|
|||
|
|
# Load checklist
|
|||
|
|
checklist_path = self._current_run_dir / "0-config" / "intent_checklist.json"
|
|||
|
|
if not checklist_path.exists():
|
|||
|
|
print("[Phase 5.5] Warning: No checklist found")
|
|||
|
|
return 0.0, None
|
|||
|
|
|
|||
|
|
with open(checklist_path, 'r', encoding='utf-8') as f:
|
|||
|
|
checklist = json.load(f)
|
|||
|
|
|
|||
|
|
# Load world building
|
|||
|
|
world_path = self._current_run_dir / "3-world" / "3-world-building.md"
|
|||
|
|
if not world_path.exists():
|
|||
|
|
print("[Phase 5.5] Warning: No world building found")
|
|||
|
|
return 1.0, None
|
|||
|
|
|
|||
|
|
with open(world_path, 'r', encoding='utf-8') as f:
|
|||
|
|
world_content = f.read()
|
|||
|
|
|
|||
|
|
# Check each item
|
|||
|
|
items = checklist.get('items', [])
|
|||
|
|
failed_items = []
|
|||
|
|
|
|||
|
|
for item in items:
|
|||
|
|
if not item.get('required', False):
|
|||
|
|
continue
|
|||
|
|
|
|||
|
|
# Simple check: see if must_be content is in world_content
|
|||
|
|
must_be = item.get('must_be', '')
|
|||
|
|
if isinstance(must_be, list):
|
|||
|
|
must_be = ' '.join(must_be)
|
|||
|
|
|
|||
|
|
# Very naive check - in real implementation would use more sophisticated matching
|
|||
|
|
if must_be and must_be != '待设定' and len(must_be) > 2:
|
|||
|
|
if must_be not in world_content:
|
|||
|
|
failed_items.append(item)
|
|||
|
|
|
|||
|
|
# Calculate deviation
|
|||
|
|
if not items:
|
|||
|
|
deviation = 0.0
|
|||
|
|
else:
|
|||
|
|
required_items = [i for i in items if i.get('required', False)]
|
|||
|
|
if not required_items:
|
|||
|
|
deviation = 0.0
|
|||
|
|
else:
|
|||
|
|
deviation = len(failed_items) / len(required_items)
|
|||
|
|
|
|||
|
|
# Generate warning if needed
|
|||
|
|
warning_path = None
|
|||
|
|
if deviation >= 0.2: # 20% or more deviation
|
|||
|
|
warning_path = self._current_run_dir / "drafts" / "alignment" / f"alignment-warning_{get_timestamp_iso().replace(':', '-')}.md"
|
|||
|
|
|
|||
|
|
warning_content = f"""# Alignment Warning
|
|||
|
|
|
|||
|
|
Deviation Score: {deviation:.1%}
|
|||
|
|
Failed Items: {len(failed_items)}/{len([i for i in items if i.get('required', False)])}
|
|||
|
|
|
|||
|
|
## Failed Checks
|
|||
|
|
"""
|
|||
|
|
for item in failed_items:
|
|||
|
|
warning_content += f"\n- **{item['name']}**: {item['description']}\n"
|
|||
|
|
warning_content += f" - Expected: {item.get('must_be', 'N/A')}\n"
|
|||
|
|
|
|||
|
|
warning_content += """
|
|||
|
|
## Recommended Actions
|
|||
|
|
1. Review world building against intent checklist
|
|||
|
|
2. Update world building to align with original intent
|
|||
|
|
3. Or update intent checklist if requirements have changed
|
|||
|
|
"""
|
|||
|
|
atomic_write_text(warning_path, warning_content)
|
|||
|
|
print(f"[Phase 5.5] Warning generated: {warning_path}")
|
|||
|
|
|
|||
|
|
print(f"[Phase 5.5] Complete: deviation = {deviation:.1%}")
|
|||
|
|
return deviation, warning_path
|
|||
|
|
|
|||
|
|
# =========================================================================
|
|||
|
|
# Run All Phases 1-5
|
|||
|
|
# =========================================================================
|
|||
|
|
|
|||
|
|
def run_all(
|
|||
|
|
self,
|
|||
|
|
book_title: str,
|
|||
|
|
genre: str,
|
|||
|
|
target_words: int,
|
|||
|
|
**kwargs
|
|||
|
|
) -> Dict[str, Any]:
|
|||
|
|
"""
|
|||
|
|
Run all phases 1-5 in sequence
|
|||
|
|
|
|||
|
|
Returns:
|
|||
|
|
Dict with paths to all generated files
|
|||
|
|
"""
|
|||
|
|
results = {}
|
|||
|
|
|
|||
|
|
# Phase 1
|
|||
|
|
run_dir, book_uid, run_id = self.phase1_initialize(
|
|||
|
|
book_title, genre, target_words, **kwargs
|
|||
|
|
)
|
|||
|
|
results['run_dir'] = run_dir
|
|||
|
|
results['book_uid'] = book_uid
|
|||
|
|
results['run_id'] = run_id
|
|||
|
|
|
|||
|
|
# Phase 2
|
|||
|
|
style_path = self.phase2_style_guide(
|
|||
|
|
**kwargs.get('style_guide', {})
|
|||
|
|
)
|
|||
|
|
results['style_guide'] = style_path
|
|||
|
|
|
|||
|
|
# Phase 3
|
|||
|
|
outline_path = self.phase3_main_outline(
|
|||
|
|
kwargs.get('outline_content')
|
|||
|
|
)
|
|||
|
|
results['main_outline'] = outline_path
|
|||
|
|
|
|||
|
|
# Phase 4
|
|||
|
|
plan_path, outlines_path = self.phase4_chapter_plan(
|
|||
|
|
kwargs.get('chapters_data')
|
|||
|
|
)
|
|||
|
|
results['chapter_plan'] = plan_path
|
|||
|
|
results['chapter_outlines'] = outlines_path
|
|||
|
|
|
|||
|
|
# Phase 5
|
|||
|
|
world_path = self.phase5_world_building(
|
|||
|
|
kwargs.get('world_content')
|
|||
|
|
)
|
|||
|
|
results['world_building'] = world_path
|
|||
|
|
|
|||
|
|
# Phase 5.5
|
|||
|
|
deviation, warning_path = self.phase5_5_alignment_check()
|
|||
|
|
results['alignment_deviation'] = deviation
|
|||
|
|
results['alignment_warning'] = warning_path
|
|||
|
|
|
|||
|
|
print("\n[Phases 1-5] All complete!")
|
|||
|
|
return results
|
|||
|
|
|
|||
|
|
|
|||
|
|
# ============================================================================
|
|||
|
|
# Module Test
|
|||
|
|
# ============================================================================
|
|||
|
|
|
|||
|
|
if __name__ == "__main__":
|
|||
|
|
import tempfile
|
|||
|
|
|
|||
|
|
print("=== Phase Runner Test (Phases 1-5) ===\n")
|
|||
|
|
|
|||
|
|
with tempfile.TemporaryDirectory() as tmpdir:
|
|||
|
|
base_dir = Path(tmpdir) / "novels"
|
|||
|
|
|
|||
|
|
# Create workspace manager
|
|||
|
|
workspace = WorkspaceManager(base_dir)
|
|||
|
|
|
|||
|
|
# Create phase runner
|
|||
|
|
runner = PhaseRunner(workspace)
|
|||
|
|
|
|||
|
|
# Test full run
|
|||
|
|
results = runner.run_all(
|
|||
|
|
book_title="阴间外卖",
|
|||
|
|
genre="都市灵异",
|
|||
|
|
target_words=100000,
|
|||
|
|
subgenre="系统流",
|
|||
|
|
mode="manual"
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
print(f"\n[Test] Run complete:")
|
|||
|
|
print(f" run_dir: {results['run_dir']}")
|
|||
|
|
print(f" book_uid: {results['book_uid']}")
|
|||
|
|
print(f" style_guide: {results['style_guide'].exists()}")
|
|||
|
|
print(f" main_outline: {results['main_outline'].exists()}")
|
|||
|
|
print(f" chapter_plan: {results['chapter_plan'].exists()}")
|
|||
|
|
print(f" world_building: {results['world_building'].exists()}")
|
|||
|
|
print(f" alignment_deviation: {results['alignment_deviation']:.1%}")
|
|||
|
|
|
|||
|
|
print("\n=== All tests completed ===")
|