diff --git a/packages/cli/src/commands/export.ts b/packages/cli/src/commands/export.ts index d6de8e58..9a3cba71 100644 --- a/packages/cli/src/commands/export.ts +++ b/packages/cli/src/commands/export.ts @@ -84,7 +84,7 @@ export const exportCommand = new Command("export") }); async function exportEpub( - book: { readonly title: string }, + book: { readonly title: string; readonly language?: string }, chapters: ReadonlyArray<{ readonly number: number; readonly wordCount: number }>, chaptersDir: string, bookId: string, @@ -112,7 +112,7 @@ async function exportEpub( } const epubInstance = new EPub( - { title: book.title, lang: "zh-CN" }, + { title: book.title, lang: book.language === "en" ? "en" : "zh-CN" }, epubChapters, ); const epubBuffer: Buffer = await epubInstance.genEpub(); diff --git a/packages/core/src/agents/architect.ts b/packages/core/src/agents/architect.ts index 546643a6..25c04a2e 100644 --- a/packages/core/src/agents/architect.ts +++ b/packages/core/src/agents/architect.ts @@ -151,12 +151,17 @@ ${eraBlock} 4. 伏笔前后呼应,不留悬空线 5. 配角有独立动机,不是工具人`; + const resolvedLanguage = book.language ?? gp.language; + const langPrefix = resolvedLanguage === "en" + ? `【LANGUAGE OVERRIDE】ALL output (story_bible, volume_outline, book_rules, current_state, pending_hooks) MUST be written in English. Character names, place names, and all prose must be in English. The === SECTION: === tags remain unchanged.\n\n` + : ""; + const userMessage = resolvedLanguage === "en" + ? `Generate the complete foundation for a ${gp.name} novel titled "${book.title}". Write everything in English.` + : `请为标题为"${book.title}"的${gp.name}小说生成完整基础设定。`; + const response = await this.chat([ - { role: "system", content: systemPrompt }, - { - role: "user", - content: `请为标题为"${book.title}"的${gp.name}小说生成完整基础设定。`, - }, + { role: "system", content: langPrefix + systemPrompt }, + { role: "user", content: userMessage }, ], { maxTokens: 16384, temperature: 0.8 }); return this.parseSections(response.content); diff --git a/packages/core/src/agents/continuity.ts b/packages/core/src/agents/continuity.ts index 4d321c20..8d34d228 100644 --- a/packages/core/src/agents/continuity.ts +++ b/packages/core/src/agents/continuity.ts @@ -253,7 +253,31 @@ export class ContinuityAuditor extends BaseAgent { ? "\n\n你有联网搜索能力(search_web / fetch_url)。对于涉及真实年代、人物、事件、地理、政策的内容,你必须用search_web核实,不可凭记忆判断。至少对比2个来源交叉验证。" : ""; - const systemPrompt = `你是一位严格的${gp.name}网络小说审稿编辑。你的任务是对章节进行连续性、一致性和质量审查。${protagonistBlock}${searchNote} + const resolvedLanguage = gp.language; + const isEnglish = resolvedLanguage === "en"; + + const systemPrompt = isEnglish + ? `You are a strict ${gp.name} web fiction editor. Audit the chapter for continuity, consistency, and quality. ALL OUTPUT MUST BE IN ENGLISH.${protagonistBlock}${searchNote} + +Audit dimensions: +${dimList} + +Output format MUST be JSON: +{ + "passed": true/false, + "issues": [ + { + "severity": "critical|warning|info", + "category": "dimension name", + "description": "specific issue description", + "suggestion": "fix suggestion" + } + ], + "summary": "one-sentence audit conclusion" +} + +passed is false ONLY when critical-severity issues exist.` + : `你是一位严格的${gp.name}网络小说审稿编辑。你的任务是对章节进行连续性、一致性和质量审查。${protagonistBlock}${searchNote} 审查维度: ${dimList} diff --git a/packages/core/src/agents/post-write-validator.ts b/packages/core/src/agents/post-write-validator.ts index 103206cb..df7379b2 100644 --- a/packages/core/src/agents/post-write-validator.ts +++ b/packages/core/src/agents/post-write-validator.ts @@ -55,6 +55,13 @@ export function validatePostWrite( ): ReadonlyArray { const violations: PostWriteViolation[] = []; + // Skip Chinese-specific rules for English content + const isEnglish = genreProfile.language === "en"; + if (isEnglish) { + // For English, only run book-specific prohibitions and paragraph length check + return validatePostWriteEnglish(content, genreProfile, bookRules); + } + // 1. 硬性禁令: "不是…而是…" 句式 if (/不是[^,。!?\n]{0,30}[,,]?\s*而是/.test(content)) { violations.push({ @@ -236,3 +243,72 @@ export function validatePostWrite( return violations; } + +/** English-specific post-write validation rules. */ +function validatePostWriteEnglish( + content: string, + genreProfile: GenreProfile, + bookRules: BookRules | null, +): ReadonlyArray { + const violations: PostWriteViolation[] = []; + + // 1. AI-tell word density (from en-prompt-sections IRON LAW 3) + const aiTellWords = ["delve", "tapestry", "testament", "intricate", "pivotal", "vibrant", "embark", "comprehensive", "nuanced"]; + for (const word of aiTellWords) { + const regex = new RegExp(`\\b${word}\\b`, "gi"); + const matches = content.match(regex); + if (matches && matches.length > Math.ceil(content.length / 3000)) { + violations.push({ + rule: "AI-tell word density", + severity: "warning", + description: `"${word}" appears ${matches.length} times (limit: 1 per 3000 chars)`, + suggestion: `Replace with a more specific word`, + }); + } + } + + // 2. Paragraph overflow (same rule applies to English) + const paragraphs = content.split(/\n\s*\n/).filter((p) => p.trim().length > 0); + const longParagraphs = paragraphs.filter((p) => p.length > 500); + if (longParagraphs.length >= 2) { + violations.push({ + rule: "Paragraph length", + severity: "warning", + description: `${longParagraphs.length} paragraphs exceed 500 characters`, + suggestion: "Break into shorter paragraphs for readability", + }); + } + + // 3. Book-specific prohibitions + if (bookRules?.prohibitions) { + for (const prohibition of bookRules.prohibitions) { + if (prohibition.length >= 2 && prohibition.length <= 50 && content.toLowerCase().includes(prohibition.toLowerCase())) { + violations.push({ + rule: "Book prohibition", + severity: "error", + description: `Found banned content: "${prohibition}"`, + suggestion: "Remove or rewrite this content", + }); + } + } + } + + // 4. Genre fatigue words + const fatigueWords = bookRules?.fatigueWordsOverride && bookRules.fatigueWordsOverride.length > 0 + ? bookRules.fatigueWordsOverride + : genreProfile.fatigueWords; + for (const word of fatigueWords) { + const regex = new RegExp(`\\b${word}\\b`, "gi"); + const matches = content.match(regex); + if (matches && matches.length > 1) { + violations.push({ + rule: "Fatigue word", + severity: "warning", + description: `"${word}" appears ${matches.length} times (max 1 per chapter)`, + suggestion: "Vary the vocabulary", + }); + } + } + + return violations; +} diff --git a/packages/core/src/agents/reviser.ts b/packages/core/src/agents/reviser.ts index 0ce24d3c..5fb3b72c 100644 --- a/packages/core/src/agents/reviser.ts +++ b/packages/core/src/agents/reviser.ts @@ -90,7 +90,12 @@ export class ReviserAgent extends BaseAgent { ? `\n\n主角人设锁定:${bookRules.protagonist.name},${bookRules.protagonist.personalityLock.join("、")}。修改不得违反人设。` : ""; - const systemPrompt = `你是一位专业的${gp.name}网络小说修稿编辑。你的任务是根据审稿意见对章节进行修正。${protagonistBlock} + const isEnglish = gp.language === "en"; + const langPrefix = isEnglish + ? `【LANGUAGE OVERRIDE】ALL output (FIXED_ISSUES, REVISED_CONTENT, UPDATED_STATE, UPDATED_HOOKS) MUST be in English. The revised chapter content must be written entirely in English.\n\n` + : ""; + + const systemPrompt = `${langPrefix}你是一位专业的${gp.name}网络小说修稿编辑。你的任务是根据审稿意见对章节进行修正。${protagonistBlock} 修稿模式:${modeDesc} diff --git a/packages/core/src/agents/settler-prompts.ts b/packages/core/src/agents/settler-prompts.ts index 072b565f..d23594fe 100644 --- a/packages/core/src/agents/settler-prompts.ts +++ b/packages/core/src/agents/settler-prompts.ts @@ -6,7 +6,10 @@ export function buildSettlerSystemPrompt( book: BookConfig, genreProfile: GenreProfile, bookRules: BookRules | null, + language?: "zh" | "en", ): string { + const resolvedLang = language ?? genreProfile.language; + const isEnglish = resolvedLang === "en"; const numericalBlock = genreProfile.numericalSystem ? `\n- 本题材有数值/资源体系,你必须在 UPDATED_LEDGER 中追踪正文中出现的所有资源变动 - 数值验算铁律:期初 + 增量 = 期末,三项必须可验算` @@ -24,7 +27,11 @@ export function buildSettlerSystemPrompt( ? `\n## 全员追踪\nPOST_SETTLEMENT 必须额外包含:本章出场角色清单、角色间关系变动、未出场但被提及的角色。` : ""; - return `你是状态追踪分析师。给定新章节正文和当前 truth 文件,你的任务是产出更新后的 truth 文件。 + const langPrefix = isEnglish + ? `【LANGUAGE OVERRIDE】ALL output (state card, hooks, summaries, subplots, emotional arcs, character matrix) MUST be in English. The === TAG === markers remain unchanged.\n\n` + : ""; + + return `${langPrefix}你是状态追踪分析师。给定新章节正文和当前 truth 文件,你的任务是产出更新后的 truth 文件。 ## 工作模式 diff --git a/packages/core/src/agents/writer.ts b/packages/core/src/agents/writer.ts index 473adeb5..5ce1c5d7 100644 --- a/packages/core/src/agents/writer.ts +++ b/packages/core/src/agents/writer.ts @@ -237,6 +237,7 @@ export class WriterAgent extends BaseAgent { }): Promise<{ settlement: ReturnType; usage: TokenUsage }> { const settlerSystem = buildSettlerSystemPrompt( params.book, params.genreProfile, params.bookRules, + params.book.language ?? params.genreProfile.language, ); const settlerUser = buildSettlerUserPrompt({ @@ -274,6 +275,7 @@ export class WriterAgent extends BaseAgent { bookDir: string, output: WriteChapterOutput, numericalSystem: boolean = true, + language: "zh" | "en" = "zh", ): Promise { const chaptersDir = join(bookDir, "chapters"); const storyDir = join(bookDir, "story"); @@ -282,8 +284,11 @@ export class WriterAgent extends BaseAgent { const paddedNum = String(output.chapterNumber).padStart(4, "0"); const filename = `${paddedNum}_${this.sanitizeFilename(output.title)}.md`; + const heading = language === "en" + ? `# Chapter ${output.chapterNumber}: ${output.title}` + : `# 第${output.chapterNumber}章 ${output.title}`; const chapterContent = [ - `# 第${output.chapterNumber}章 ${output.title}`, + heading, "", output.content, ].join("\n"); diff --git a/packages/core/src/pipeline/runner.ts b/packages/core/src/pipeline/runner.ts index 459996b7..fbf30f5c 100644 --- a/packages/core/src/pipeline/runner.ts +++ b/packages/core/src/pipeline/runner.ts @@ -273,10 +273,14 @@ export class PipelineRunner { const filename = `${paddedNum}_${sanitized}.md`; const filePath = join(chaptersDir, filename); - await writeFile(filePath, `# 第${chapterNumber}章 ${output.title}\n\n${output.content}`, "utf-8"); + const resolvedLang = book.language ?? gp.language; + const heading = resolvedLang === "en" + ? `# Chapter ${chapterNumber}: ${output.title}` + : `# 第${chapterNumber}章 ${output.title}`; + await writeFile(filePath, `${heading}\n\n${output.content}`, "utf-8"); // Save truth files - await writer.saveChapter(bookDir, output, gp.numericalSystem); + await writer.saveChapter(bookDir, output, gp.numericalSystem, resolvedLang); await writer.saveNewTruthFiles(bookDir, output); // Update index @@ -655,9 +659,13 @@ export class PipelineRunner { const title = output.title; const filename = `${paddedNum}_${title.replace(/[/\\?%*:|"<>]/g, "").replace(/\s+/g, "_").slice(0, 50)}.md`; + const pipelineLang = book.language ?? gp.language; + const pipelineHeading = pipelineLang === "en" + ? `# Chapter ${chapterNumber}: ${title}` + : `# 第${chapterNumber}章 ${title}`; await writeFile( join(chaptersDir, filename), - `# 第${chapterNumber}章 ${title}\n\n${finalContent}`, + `${pipelineHeading}\n\n${finalContent}`, "utf-8", );