fix: English language pipeline across all agents

- Architect: language override prefix forces English output for story_bible, outline, book_rules
- Settler: language override prefix forces English truth files
- Auditor: bilingual system prompt (English for en genres)
- Reviser: language override prefix forces English revised content
- Post-write validator: skip Chinese-only rules for English, add English AI-tell checks
- Writer saveChapter: "Chapter X:" heading for English, "第X章" for Chinese
- Runner: pass language to saveChapter and chapter file headings
- Export: epub lang tag respects book language setting
This commit is contained in:
Ma
2026-03-18 22:44:19 +08:00
parent 1fc99a0f26
commit 0f8033c701
8 changed files with 144 additions and 14 deletions
+2 -2
View File
@@ -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();
+10 -5
View File
@@ -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);
+25 -1
View File
@@ -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}
@@ -55,6 +55,13 @@ export function validatePostWrite(
): ReadonlyArray<PostWriteViolation> {
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<PostWriteViolation> {
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;
}
+6 -1
View File
@@ -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}
+8 -1
View File
@@ -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 文件。
## 工作模式
+6 -1
View File
@@ -237,6 +237,7 @@ export class WriterAgent extends BaseAgent {
}): Promise<{ settlement: ReturnType<typeof parseSettlementOutput>; 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<void> {
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");
+11 -3
View File
@@ -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",
);