mirror of
https://github.com/Narcooo/inkos.git
synced 2026-08-28 14:57:58 +08:00
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:
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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}
|
||||
|
||||
|
||||
@@ -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 文件。
|
||||
|
||||
## 工作模式
|
||||
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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",
|
||||
);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user