feat: 插图提示词萃取脚本(pnpm art:prompts → 57 张需求单文件,供 ChatGPT 网页版批量出图)

This commit is contained in:
Jayway Liu
2026-10-03 18:04:00 +08:00
parent dc76f47a8e
commit d2a31ab38c
3 changed files with 428 additions and 1 deletions
+162
View File
@@ -0,0 +1,162 @@
/**
* 插图提示词萃取:从 content/chapters/*.json 提取需要配插图的场景,
* 生成一份可一次性上传给 ChatGPT 网页版的提示词文件(docs/art/illustration-prompts.md)。
*
* 运行:pnpm art:prompts
*
* 萃取范围:
* - P0 讲述点场景变体(narration.variants,情感高潮戏,最优先)
* - P0 过场(cutscene)
* - P1 章节主视觉(每章一张)
* - P2 场景对话节点(dialogue 且 sprite="scene")
* 编号与章节/节点 ID 的映射表附在文末,下载后按编号重命名归位。
*/
import { readFileSync, writeFileSync, mkdirSync } from 'node:fs';
import { join, dirname } from 'node:path';
import { fileURLToPath } from 'node:url';
const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..');
interface SceneVariant { signature: string; variantId: string; label: string; tint?: string }
interface StoryNode {
id: string; type: string;
speaker?: string; text?: string; sprite?: string;
pointId?: string; mechanism?: string;
variants?: SceneVariant[];
video?: string; caption?: string;
}
interface Chapter { id: string; act: number; title: string; nodes: StoryNode[] }
interface ArtItem {
code: string; // 出图编号,如 C1-V01
priority: 'P0' | 'P1' | 'P2';
source: string; // 来源(章节/节点/变体)
filename: string; // 建议保存路径
scene: string; // 画面说明(中文,喂给出图模型)
}
const CHAPTERS = ['chapter1', 'chapter2', 'chapter3'];
/** 清掉文案里的玩家提示括号与条件标记,只留画面内容 */
function cleanSceneText(text: string): string {
return text
.replace(/【若[^】]*】/g, '')
.replace(/([^)]*(?:点击|确认|知晓|提示|按住|选择|视为)[^)]*)/g, '')
.replace(/\s+/g, ' ')
.trim();
}
function truncate(text: string, max = 220): string {
return text.length > max ? text.slice(0, max) + '……' : text;
}
const items: ArtItem[] = [];
for (const chId of CHAPTERS) {
const chapter = JSON.parse(readFileSync(join(ROOT, 'content/chapters', `${chId}.json`), 'utf-8')) as Chapter;
const chNum = chId.replace('chapter', 'C');
items.push({
code: `${chNum}-KV`,
priority: 'P1',
source: `${chId} 章节主视觉`,
filename: `assets/art/${chId}/key-visual.png`,
scene: `章节主视觉:${chapter.title}。考古悬疑题材的主视觉构图,留出标题空间。`,
});
let sceneIdx = 0, variantIdx = 0, cutIdx = 0;
const seen = new Set<string>();
for (const node of chapter.nodes) {
if (node.type === 'narration' && node.variants) {
for (const v of node.variants) {
variantIdx += 1;
const code = `${chNum}-V${String(variantIdx).padStart(2, '0')}`;
items.push({
code,
priority: 'P0',
source: `${node.pointId}(${node.mechanism})变体 ${v.variantId} · 情绪签名「${v.signature}」`,
filename: `assets/art/${chId}/${node.pointId}-${v.variantId}.png`,
scene: cleanSceneText(v.label),
});
}
} else if (node.type === 'cutscene') {
cutIdx += 1;
items.push({
code: `${chNum}-CS${cutIdx}`,
priority: 'P0',
source: `${node.id} 过场(${node.video})`,
filename: `assets/art/${chId}/${node.id}-cutscene.png`,
scene: cleanSceneText(node.caption ?? ''),
});
} else if (node.type === 'dialogue' && node.sprite === 'scene' && node.text) {
const scene = cleanSceneText(node.text);
if (scene.length < 20 || seen.has(scene)) continue;
seen.add(scene);
sceneIdx += 1;
const isOpen = node.id.includes('open');
items.push({
code: `${chNum}-S${String(sceneIdx).padStart(2, '0')}`,
priority: isOpen ? 'P1' : 'P2',
source: `${node.id} 场景旁白`,
filename: `assets/art/${chId}/${node.id}.png`,
scene: truncate(scene),
});
}
}
}
const STYLE = [
'统一美术风格(每张图都严格套用):水彩手绘漫画质感,暗色调悬疑氛围,',
'电影级光影与构图,克制的暖冷对比;考古现场的旧照片颗粒感与奇幻微光结合。',
'横幅 16:9。画面中不出现任何文字、字母、数字、水印。',
'人物一律以中远景、背影或剪影出现,不要正脸特写(角色立绘后续单独处理,靠远景保持角色一致性)。',
].join('');
const HEADER = `# 《苍梧疑冢》Act 1 插图批量生成任务
你好。附件/下文是一份互动叙事游戏的插图需求清单,共 ${items.length} 张。
请按以下规则逐张生成插图:
## 全局风格
${STYLE}
## 工作规则
1. 每个编号条目生成**一张**插图,按编号顺序来(编号即优先级,P0 最先)。
2. 每条目的"画面"字段是场景描述,请在不违背风格的前提下自由构图;
描述若引用剧情文本,取其中的**视觉内容**(环境、光线、器物、氛围),忽略叙事性语句。
3. 每条目生成后,先单独展示这张图并标注其编号(如"C1-V01"),便于我逐张下载核对。
4. 每批生成 4 张后停下来,等我回复"继续"再生成下一批——避免触发生成限额导致风格漂移。
5. 如果某条的描述让你不确定画什么,不要自己发明剧情元素,按最接近描述字面意思的静物/空镜处理。
---
## 插图清单
`;
const PRIORITY_ORDER: Record<ArtItem['priority'], number> = { P0: 0, P1: 1, P2: 2 };
const sorted = [...items].sort((a, b) => PRIORITY_ORDER[a.priority] - PRIORITY_ORDER[b.priority]);
const sections: string[] = [];
let currentPriority = '';
for (const item of sorted) {
if (item.priority !== currentPriority) {
currentPriority = item.priority;
const label = { P0: 'P0 · 讲述点变体与过场(情感高潮,最优先)', P1: 'P1 · 章节主视觉与开场场景', P2: 'P2 · 一般场景' }[item.priority];
sections.push(`\n### ${label}\n`);
}
sections.push(`**${item.code}**\n画面:${item.scene}\n`);
}
const MAPPING = `\n---\n\n## 编号映射表(出图后按"建议文件名"重命名归位)\n\n| 编号 | 来源 | 建议文件名 |\n|---|---|---|\n${sorted.map((i) => `| ${i.code} | ${i.source} | ${i.filename} |`).join('\n')}\n`;
mkdirSync(join(ROOT, 'docs/art'), { recursive: true });
const outPath = join(ROOT, 'docs/art/illustration-prompts.md');
writeFileSync(outPath, HEADER + sections.join('\n') + MAPPING, 'utf-8');
const counts = { P0: 0, P1: 0, P2: 0 };
for (const i of sorted) counts[i.priority] += 1;
console.log(`✓ 共 ${items.length} 张插图需求(P0:${counts.P0} P1:${counts.P1} P2:${counts.P2})`);
console.log(`✓ 输出:${outPath}`);