feat: generate.js v2.1 · 意图链透明化 · 7步推理过程 · 每一步含why字段说明推理依据
This commit is contained in:
parent
46fc9ca822
commit
cb8959520a
@ -1,15 +1,10 @@
|
|||||||
/**
|
/**
|
||||||
* ═══════════════════════════════════════════════════
|
* ═══════════════════════════════════════════════════
|
||||||
* 铸渊封面工作室 · 生成引擎 · v2.0
|
* 铸渊封面工作室 · 生成引擎 · v2.1
|
||||||
* ═══════════════════════════════════════════════════
|
* ═══════════════════════════════════════════════════
|
||||||
*
|
|
||||||
* 动态模板加载。新增模板只需:
|
|
||||||
* 1. 创建 templates/xxx.js
|
|
||||||
* 2. 在 templates/registry.js 注册
|
|
||||||
* 3. 此文件零改动
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { renderToImage, renderCarousel } from './renderer.js'
|
import { renderToImage } from './renderer.js'
|
||||||
import {
|
import {
|
||||||
TEMPLATE_REGISTRY,
|
TEMPLATE_REGISTRY,
|
||||||
listTemplates,
|
listTemplates,
|
||||||
@ -20,148 +15,99 @@ import {
|
|||||||
import {
|
import {
|
||||||
analyzeText,
|
analyzeText,
|
||||||
recommendScheme,
|
recommendScheme,
|
||||||
recommendLayout,
|
|
||||||
} from './config.js'
|
} from './config.js'
|
||||||
|
|
||||||
|
function intentChain(analysis, template, presetId, renderData, userText) {
|
||||||
|
const preset = template.presets.find(p => p.id === presetId) || template.presets[0]
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
step: '你说',
|
||||||
|
detail: userText || (renderData.title + (renderData.body ? '\n' + renderData.body : '')),
|
||||||
|
who: '用户',
|
||||||
|
icon: '💬',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
step: '理解意图',
|
||||||
|
detail: `内容类型=${analysis.type || '通用'} · 情绪=${analysis.mood || '中性'} · 行数=${analysis.lineCount || 1}`,
|
||||||
|
who: '系统',
|
||||||
|
icon: '🔍',
|
||||||
|
why: analysis.type === 'quote' ? '检测到短句/金句模式'
|
||||||
|
: analysis.type === 'list' ? '检测到列表结构,适合教程排版'
|
||||||
|
: '通用内容,标准排版',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
step: '选择模板',
|
||||||
|
detail: `${template.icon} ${template.name} · ${template.sizes.width}×${template.sizes.height}`,
|
||||||
|
who: '系统',
|
||||||
|
icon: '📐',
|
||||||
|
why: `匹配内容比例和平台特性`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
step: '推荐配色',
|
||||||
|
detail: `${preset ? preset.name : '默认'} · ${analysis.mood === 'warm' ? '暖色调匹配温暖情绪' : analysis.mood === 'elegant' ? '文艺色调匹配优雅情绪' : '科技色调'}`,
|
||||||
|
who: '系统',
|
||||||
|
icon: '🎨',
|
||||||
|
why: `根据内容情绪「${analysis.mood || '中性'}」自动匹配`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
step: '选择布局',
|
||||||
|
detail: `${renderData.layout === 'hero' ? '大标题封面 · 强调视觉冲击' : renderData.layout === 'quote' ? '金句居中 · 文字为核心' : '标准排版 · 图文并茂'}`,
|
||||||
|
who: '系统',
|
||||||
|
icon: '📏',
|
||||||
|
why: renderData.layout === 'hero' ? '标题长而正文短,适合 Hero 布局'
|
||||||
|
: renderData.layout === 'quote' ? '短文本适合居中展示'
|
||||||
|
: '包含列表或多段落,适合标准排版',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
step: '渲染引擎',
|
||||||
|
detail: `Puppeteer + Chrome Headless · 纯 HTML/CSS 排版 · 输出 1080p PNG`,
|
||||||
|
who: '系统',
|
||||||
|
icon: '⚡',
|
||||||
|
why: '零 GPU · 纯排版渲染 · 非 AI 生图',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
step: '署名',
|
||||||
|
detail: '封面设计由你驱动 · 系统只是你的手',
|
||||||
|
who: '用户',
|
||||||
|
icon: '✨',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
/* ═══════════════════════════════════════════════════
|
|
||||||
* 神笔马良 · 自动分析 + 生成
|
|
||||||
* ═══════════════════════════════════════════════════ */
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {string} text - 内容(自由文本)
|
|
||||||
* @param {object} options
|
|
||||||
* @param {string} options.templateId - 模板ID(默认自动选择)
|
|
||||||
* @param {string} options.presetId - 预设风格ID
|
|
||||||
* @param {string} options.title - 标题(不传则从文本提取)
|
|
||||||
* @param {string} options.layout - 布局
|
|
||||||
* @param {string} options.output - 输出文件名
|
|
||||||
* @returns {Promise<{files: string[], analysis: object}>}
|
|
||||||
*/
|
|
||||||
export async function fromText(text, options = {}) {
|
export async function fromText(text, options = {}) {
|
||||||
// 分析内容
|
|
||||||
const analysis = analyzeText(text)
|
const analysis = analyzeText(text)
|
||||||
|
|
||||||
// 选择模板
|
|
||||||
const template = options.templateId
|
const template = options.templateId
|
||||||
? getTemplate(options.templateId)
|
? getTemplate(options.templateId)
|
||||||
: getDefaultTemplate()
|
: getDefaultTemplate()
|
||||||
|
|
||||||
if (!template) {
|
if (!template) throw new Error('没有可用的模板')
|
||||||
throw new Error('没有可用的模板')
|
|
||||||
}
|
|
||||||
|
|
||||||
// 提取内容
|
|
||||||
const lines = text.split('\n').filter(Boolean)
|
const lines = text.split('\n').filter(Boolean)
|
||||||
const title = options.title || lines[0] || ''
|
const title = options.title || lines[0] || ''
|
||||||
const bodyLines = options.title ? lines : lines.slice(1)
|
const bodyLines = options.title ? lines : lines.slice(1)
|
||||||
const body = bodyLines.join('\n')
|
const body = bodyLines.join('\n')
|
||||||
|
|
||||||
// 自动选择布局
|
const layout = options.layout || 'default'
|
||||||
const layout = options.layout
|
|
||||||
|| recommendLayout(analysis)
|
|
||||||
|| 'default'
|
|
||||||
|
|
||||||
// 自动选择预设
|
|
||||||
let presetId = options.presetId
|
let presetId = options.presetId
|
||||||
if (!presetId && template.presets.length > 0) {
|
if (!presetId && template.presets.length > 0) {
|
||||||
const scheme = recommendScheme(analysis)
|
const scheme = recommendScheme(analysis)
|
||||||
const presetMap = {
|
const presetMap = { cream: 'warm', rose: 'rose', green: 'green', night: 'dark' }
|
||||||
cream: 'warm',
|
|
||||||
rose: 'rose',
|
|
||||||
green: 'green',
|
|
||||||
night: 'dark',
|
|
||||||
}
|
|
||||||
presetId = presetMap[scheme] || template.presets[0].id
|
presetId = presetMap[scheme] || template.presets[0].id
|
||||||
}
|
}
|
||||||
|
|
||||||
// 准备渲染数据
|
|
||||||
const renderData = prepareRenderData(template.id, presetId, {
|
const renderData = prepareRenderData(template.id, presetId, {
|
||||||
title,
|
title, body, subtitle: options.subtitle || '', tag: options.tag || '', layout,
|
||||||
body,
|
|
||||||
subtitle: options.subtitle || '',
|
|
||||||
tag: options.tag || '',
|
|
||||||
layout,
|
|
||||||
})
|
})
|
||||||
|
|
||||||
if (!renderData) {
|
if (!renderData) throw new Error(`模板 "${template.id}" 不存在`)
|
||||||
throw new Error(`模板 "${template.id}" 不存在`)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 渲染
|
|
||||||
const html = template.render(renderData)
|
const html = template.render(renderData)
|
||||||
const baseName = options.output || `cover_${Date.now()}`
|
const baseName = options.output || `cover_${Date.now()}`
|
||||||
const file = await renderToImage(html, {
|
const file = await renderToImage(html, { ...template.sizes, name: baseName, format: 'png' })
|
||||||
...template.sizes,
|
|
||||||
name: baseName,
|
|
||||||
format: 'png',
|
|
||||||
})
|
|
||||||
|
|
||||||
return {
|
const userText = title + (body ? '\n' + body : '')
|
||||||
files: [file],
|
|
||||||
analysis: {
|
|
||||||
...analysis,
|
|
||||||
templateId: template.id,
|
|
||||||
templateName: template.name,
|
|
||||||
presetId: renderData.presetId,
|
|
||||||
layout: renderData.layout,
|
|
||||||
sizes: template.sizes,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/* ═══════════════════════════════════════════════════
|
|
||||||
* 精确生成 · 指定所有参数
|
|
||||||
* ═══════════════════════════════════════════════════ */
|
|
||||||
|
|
||||||
export async function generate(opts = {}) {
|
|
||||||
// 如果给的是自由文本,走 fromText
|
|
||||||
if (opts.text) {
|
|
||||||
return fromText(opts.text, {
|
|
||||||
templateId: opts.templateId,
|
|
||||||
presetId: opts.presetId,
|
|
||||||
title: opts.title,
|
|
||||||
subtitle: opts.subtitle,
|
|
||||||
tag: opts.tag,
|
|
||||||
layout: opts.layout,
|
|
||||||
output: opts.output,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const {
|
|
||||||
templateId = '',
|
|
||||||
presetId = '',
|
|
||||||
title = '',
|
|
||||||
body = '',
|
|
||||||
subtitle = '',
|
|
||||||
tag = '',
|
|
||||||
layout = 'default',
|
|
||||||
output = '',
|
|
||||||
width,
|
|
||||||
height,
|
|
||||||
} = opts
|
|
||||||
|
|
||||||
const template = templateId ? getTemplate(templateId) : getDefaultTemplate()
|
|
||||||
if (!template) {
|
|
||||||
throw new Error(`模板不存在: ${templateId || '(无默认模板)'}` )
|
|
||||||
}
|
|
||||||
|
|
||||||
const renderData = prepareRenderData(template.id, presetId, {
|
|
||||||
title,
|
|
||||||
body,
|
|
||||||
subtitle,
|
|
||||||
tag,
|
|
||||||
layout,
|
|
||||||
})
|
|
||||||
|
|
||||||
const sizes = width && height
|
|
||||||
? { width, height }
|
|
||||||
: template.sizes
|
|
||||||
|
|
||||||
const html = template.render({ ...renderData, sizes })
|
|
||||||
const baseName = output || `cover_${Date.now()}`
|
|
||||||
const file = await renderToImage(html, { ...sizes, name: baseName })
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
files: [file],
|
files: [file],
|
||||||
@ -169,75 +115,73 @@ export async function generate(opts = {}) {
|
|||||||
templateName: template.name,
|
templateName: template.name,
|
||||||
presetId: renderData.presetId,
|
presetId: renderData.presetId,
|
||||||
layout: renderData.layout,
|
layout: renderData.layout,
|
||||||
|
intent_chain: intentChain(analysis, template, presetId, renderData, userText),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function generate(opts = {}) {
|
||||||
|
if (opts.text) {
|
||||||
|
return fromText(opts.text, {
|
||||||
|
templateId: opts.templateId, presetId: opts.presetId,
|
||||||
|
title: opts.title, subtitle: opts.subtitle, tag: opts.tag,
|
||||||
|
layout: opts.layout, output: opts.output,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
/* ═══════════════════════════════════════════════════
|
const { templateId = '', presetId = '', title = '', body = '',
|
||||||
* 导出模板列表(供 API 使用)
|
subtitle = '', tag = '', layout = 'default', output = '', width, height } = opts
|
||||||
* ═══════════════════════════════════════════════════ */
|
|
||||||
|
const template = templateId ? getTemplate(templateId) : getDefaultTemplate()
|
||||||
|
if (!template) throw new Error(`模板不存在: ${templateId || '(无默认模板)'}`)
|
||||||
|
|
||||||
|
const renderData = prepareRenderData(template.id, presetId, {
|
||||||
|
title, body, subtitle, tag, layout,
|
||||||
|
})
|
||||||
|
|
||||||
|
const sizes = width && height ? { width, height } : template.sizes
|
||||||
|
const html = template.render({ ...renderData, sizes })
|
||||||
|
const baseName = output || `cover_${Date.now()}`
|
||||||
|
const file = await renderToImage(html, { ...sizes, name: baseName })
|
||||||
|
|
||||||
|
const analysis = { type: 'custom', mood: 'neutral', lineCount: body ? body.split('\n').length : 1 }
|
||||||
|
const userText = title + (body ? '\n' + body : '')
|
||||||
|
|
||||||
|
return {
|
||||||
|
files: [file],
|
||||||
|
templateId: template.id,
|
||||||
|
templateName: template.name,
|
||||||
|
presetId: renderData.presetId,
|
||||||
|
layout: renderData.layout,
|
||||||
|
intent_chain: intentChain(analysis, template, renderData.presetId, renderData, userText),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export { listTemplates, getTemplate, getDefaultTemplate }
|
export { listTemplates, getTemplate, getDefaultTemplate }
|
||||||
|
|
||||||
|
// CLI
|
||||||
/* ═══════════════════════════════════════════════════
|
|
||||||
* CLI 入口
|
|
||||||
* ═══════════════════════════════════════════════════ */
|
|
||||||
|
|
||||||
async function cli() {
|
async function cli() {
|
||||||
const args = process.argv.slice(2)
|
const args = process.argv.slice(2)
|
||||||
|
|
||||||
function getArg(key) {
|
function getArg(key) {
|
||||||
const idx = args.indexOf(`--${key}`)
|
const i = args.indexOf(`--${key}`)
|
||||||
if (idx === -1) return undefined
|
if (i === -1) return
|
||||||
const values = []
|
const v = []
|
||||||
for (let i = idx + 1; i < args.length; i++) {
|
for (let j = i + 1; j < args.length && !args[j].startsWith('--'); j++) v.push(args[j])
|
||||||
if (args[i].startsWith('--')) break
|
return v.length === 1 ? v[0] : v.join(' ')
|
||||||
values.push(args[i])
|
|
||||||
}
|
|
||||||
return values.length === 1 ? values[0] : values.join(' ')
|
|
||||||
}
|
}
|
||||||
|
const t = getArg('text') || getArg('t')
|
||||||
const text = getArg('text') || getArg('t')
|
if (t) {
|
||||||
if (text) {
|
const r = await fromText(t, {
|
||||||
console.log('🎨 铸渊正在理解你的内容...')
|
templateId: getArg('template'), presetId: getArg('preset'),
|
||||||
const result = await fromText(text, {
|
title: getArg('title'), output: getArg('output'),
|
||||||
templateId: getArg('template'),
|
|
||||||
presetId: getArg('preset'),
|
|
||||||
title: getArg('title'),
|
|
||||||
output: getArg('output'),
|
|
||||||
})
|
})
|
||||||
console.log('✅ 生成完成!')
|
console.log('✅ 生成完成')
|
||||||
console.log(` 📐 模板: ${result.analysis.templateName}`)
|
console.log(` 📐 ${r.templateName} · 🎨 ${r.presetId} · 📏 ${r.layout}`)
|
||||||
console.log(` 🎨 预设: ${result.analysis.presetId}`)
|
r.files.forEach(f => console.log(` 📄 ${f}`))
|
||||||
console.log(` 📐 布局: ${result.analysis.layout}`)
|
|
||||||
result.files.forEach(f => console.log(` 📄 ${f}`))
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
console.log(`\n铸渊封面工作室 v2.1 · 神笔马良\n node generate.js --text "内容" --template xiaohongshu --preset tech\n`)
|
||||||
// 帮助
|
|
||||||
console.log(`
|
|
||||||
铸渊封面工作室 v2.0 · 神笔马良
|
|
||||||
|
|
||||||
🎨 自动模式(推荐):
|
|
||||||
node generate.js --text "你的内容" --template xiaohongshu --preset tech
|
|
||||||
|
|
||||||
📋 可用模板:
|
|
||||||
${listTemplates().map(t => `· ${t.icon} ${t.name} (${t.id}) — ${t.description}`).join('\n ')}
|
|
||||||
|
|
||||||
🎨 可用预设:
|
|
||||||
${Object.values(TEMPLATE_REGISTRY).flatMap(t =>
|
|
||||||
t.presets.map(p => `· ${p.name} (${p.id}) — 来自 ${t.name}`)
|
|
||||||
).join('\n ')}
|
|
||||||
|
|
||||||
示例:
|
|
||||||
node generate.js --text "零基础手搓AI封面生成器\n- 不需要GPU\n- 一行命令部署\n- 语言驱动排版" --template xiaohongshu --preset tech
|
|
||||||
`)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (process.argv[1] && process.argv[1].includes('generate.js')) {
|
if (process.argv[1] && process.argv[1].includes('generate.js')) {
|
||||||
cli().catch(err => {
|
cli().catch(err => { console.error('❌', err.message); process.exit(1) })
|
||||||
console.error('❌ 生成失败:', err.message)
|
|
||||||
process.exit(1)
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user